spring security defining user registration with ROLE_USER - spring

I am making a project in Spring MVC 4 with Hibernate and Spring Security. In this project I have 3 roles: ROLE_USER, ROLE_COMPANY and ROLE_ADMIN.
User will register like regular registration site, but I am confused on how to save a new user in database through registration process, that how to save the new user and database defined by Spring Security and how to fetch that information using hibernate.
Thank you.

You would have your User class that implements UserDetails which has either one or many authorities. For example:
User
#Entity
#Table(name = "User")
public class User implements UserDetails {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#NotNull
private String username;
#NotNull
private String password;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user", fetch = FetchType.EAGER, orphanRemoval = true)
private Set<UserAuthority> authorities;
//helper method to set roles for this user
public void grantRole(UserRole role) {
if (authorities == null) {
authorities = new HashSet<UserAuthority>();
}
authorities.add(role.asAuthorityFor(this));
}
//overrides, getters, setters
}
UserAuthority
#Entity
#IdClass(UserAuthority.class)
public class UserAuthority implements GrantedAuthority {
#NotNull
#ManyToOne(fetch = FetchType.LAZY)
#JsonIgnore
#Id
private User user;
#NotNull
#Id
private String authority;
//overrides, getters, setters
}
UserRole
public enum UserRole {
USER, COMPANY, ADMIN;
}
While creating user just:
User user = new User();
user.grantRole(UserRole.USER);
repository.save(user);
As for authenticating you need to implement UserDetailsService that loads the user from the repository
UserDetailsService implementation
#Service
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService {
#Autowired
private UserRepository repository;
private final AccountStatusUserDetailsChecker detailsChecker = new AccountStatusUserDetailsChecker();
#Override
public final User loadUserByUsername(String username) throws UsernameNotFoundException {
final User user = repository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
detailsChecker.check(user);
return user;
}
}
Now in your Security configuration you just use that UserDetailsService
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
#Override
protected UserDetailsService userDetailsService() {
return userDetailsService;
}
How you fetch the data is up to you, I would be using Spring Data JPA for that.

Related

How can I Integrate SpringSecuirty to My SpringBootTest?

I'm trying to test a comment_post method.
Comment has many - to - one relationship with User Entity which comes from Spring Security.
I connected this relationship by using Principal.
I think I made it working properly, but having trouble applying it to test.
Problem is that Comment Posting method gets user by finding User in Repository using Principal's email attribute, So I need to apply SecurityContext to test,
but I have no idea how to apply this function to test.
By Searching, I found out that I can make SpringSecurityContext by #WithSecurityContext
annotation, so I'm trying to apply it but having this error
java.lang.RuntimeException: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springboot.web.CommentsApiControllerTest$WithUserDetailsSecurityContextFactory': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'springboot.web.CommentsApiControllerTest' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
I'm not even sure that my approach is correct.
tbh, I kind of feel lost, maybe it's because I'm new to SpringBoot, also Security.
Here's my codes.
CommentService
#RequiredArgsConstructor
#Service
public class CommentService {
private final CommentRepository commentRepository;
private final PostsRepository postsRepository;
private final UserDetailService userDetailService;
#Transactional
public Long commentSave(CommentSaveRequestDto requestDto, Long id) {
Posts post = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다"));
requestDto.setPosts(post);
User user = userDetailService.returnUser();
requestDto.setUser(user);
return commentRepository.save(requestDto.toEntity()).getId();
}
`
UserDetailService
#RequiredArgsConstructor
#Service
public class UserDetailService {
private final UserRepository userRepository;
public User returnUser() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String userName;
if (principal instanceof UserDetails) {
userName = ((UserDetails) principal).getUsername();
} else {
userName = principal.toString();
}
int start = userName.indexOf("email")+6;
int end = userName.indexOf(".com,")+4;
String email = userName.substring(start, end);
User user = userRepository.findByEmail(email).orElse(null);
return user;
}
CommentSaveRequestDto
#Data
#NoArgsConstructor
#Builder
#AllArgsConstructor
public class CommentSaveRequestDto {
private String comment;
private Posts posts;
private User user;
/* Dto -> Entity */
public Comment toEntity() {
return Comment.builder()
.comment(comment)
.posts(posts)
.user(user)
.build();
}
}
And here is my CommentsApiControllrTest
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#Transactional
public class CommentsApiControllerTest {
#LocalServerPort
private int port;
#Autowired
private PostsRepository postsRepository;
#Autowired
private CommentRepository commentRepository;
#Autowired
private UserRepository userRepository;
#Autowired
private PostsService postsService;
#Autowired
private CommentService commentService;
#Autowired
private UserDetailService userDetailsService;
#Autowired
private WebApplicationContext context;
#Autowired ObjectMapper objectMapper;
private MockMvc mvc;
#Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.apply(sharedHttpSession())
.build();
}
#Retention(RetentionPolicy.RUNTIME)
#WithSecurityContext(factory = WithUserDetailsSecurityContextFactory.class)
public #interface WithMockCustomUser {
String name() default "testName";
String email() default "testemail#gmail.com";
Role role() default Role.USER;
}
final class WithUserDetailsSecurityContextFactory implements WithSecurityContextFactory<WithUserDetails> {
private final UserDetailsService userDetailsService;
#Autowired
public WithUserDetailsSecurityContextFactory(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public org.springframework.security.core.context.SecurityContext createSecurityContext(WithUserDetails withUser) {
String username = withUser.value();
Assert.hasLength(username, "value() must be non-empty String");
UserDetails principal = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
#After
public void tearDown() throws Exception {
postsRepository.deleteAll();
commentRepository.deleteAll();
}
#Test
#WithMockCustomUser
#Transactional // 프록시 객체에 실제 데이터를 불러올 수 있게 영속성 컨텍스트에서 관리
public void comment_등록() throws Exception {
// given
String title = "title";
String content = "content";
User user = userRepository.save(User.builder()
.name("name")
.email("fake#naver.com")
.picture("fakePic.com")
.role(Role.USER)
.build());
PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
.title(title)
.content(content)
.user(user)
.build();
postsRepository.save(requestDto.toEntity());
String comment = "comment";
Posts posts = postsRepository.findAll().get(0);
CommentSaveRequestDto saveRequestDto = CommentSaveRequestDto.builder()
.comment(comment)
.posts(posts)
.build();
Long id = posts.getId();
String url = "http://localhost:"+ port + "/api/posts/" + id + "/comments";
//when
mvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(objectMapper.writeValueAsString(saveRequestDto)))
.andExpect(status().isOk())
.andDo(print());
}
All I want is to make a mock Security User in test, so that
User user = userDetailService.returnUser();
this line in CommentService don't make any error.
Just a little tip would be really helpful to me.
Thank you in advance.

How to provide custom UserDetails with additional fields for testing a secured controller method?

Assume I have the following #WebMvcTest and #RestController in a Spring boot applcation (version 2.4.2).
// the test
#Test
#WithUserDetails
public void should_return_ok() throws Exception {
mockMvc.perform(get("/api/products").andExpect(status().isOk());
}
// the controller
#GetMapping(path = "/api/products")
public ResponseEntity<List<Product>> getProducts(#AuthenticationPrincipal CustomUserDetails userDetails) {
List<Product> products = productService.getProductsByUserId(userDetails.getUserId());
return ResponseEntity.ok(products);
}
I also provided a CustomUserDetails class which adds a userId.
#Getter
#Setter
public class CustomUserDetails extends User {
private static final long serialVersionUID = 5540615754152379571L;
private Long userId;
public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
I understand that Spring provides the #WithUserDetails annotation to provide an adequate object for testing. And this also allows specifying a custom username, password, etc. However I don't know how I could provide the userId which is necessary so that the controller method can extract it from the CustomUserDetails object.
You can create your own custom UserDetails object in your test class and do the following:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
CustomUserDetails customUserDetails = new CustomUserDetails(...);
mockMvc.perform(get("/api/products").with(user(customUserDetails))).andExpect(status().isOk());
In your implementation of UserDetailsService you should return your instance of UserDetails. For example:
#Override
public UserDetails loadByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Username " + username + " not found");
}
CustomUserDetails customUserDetails = new CustomUserDetails(user);
customUserDetails.setUserId(user.getUserId());
return customUserDetails;
}
public class CustomUserDetails implements UserDetails {
private final Long userId;
private final User user;
...constructors
...getters and setters
}
In your code, you can cast the Authentication object to your CustomUserDetails.
CustomUserDetails customUserDetails = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication();
Long userId = customUserDetails.getUserId();

Springboot multiple login

I'm trying to enable multiple login instead of single person login.
I've developed single person login by following however, don't know how to do multiple login. Anyone please help?
Account.java file:
#Getter
#Setter
public class Account {
private Long id;
private String studentId;
private String password;
}
This is my controller.
#GetMapping("/create") was made to check whether the password is properly hashed or not.
#RestController
public class AccountController {
#Autowired
AccountService accountService;
#GetMapping("/create")
public Account create(){
Account account = new Account();
account.setStudentId("123");
account.setPassword("123");
return accountService.save(account);
}
}
This is my service layer
#Service
public class AccountService implements UserDetailsService {
#Autowired
private AccountRepository accountRepository;
#Autowired
private PasswordEncoder passwordEncoder;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountRepository.findByStudentId(username);
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new User(account.getStudentId(), account.getPassword(), authorities);
}
public Account save(Account account) {
account.setPassword(passwordEncoder.encode(account.getPassword()));
return accountRepository.save(account);
}
}
This is my repository setting
#Repository
public class AccountRepository {
private Map<String, Account> accounts = new HashMap<>();
private Random random = new Random();
public Account save(Account account) {
account.setId(random.nextLong());
accounts.put(account.getStudentId(), account);
return account;
}
public Account findByStudentId(String username) {
return accounts.get(username);
}
}
How can I enable multiple users login?
Few tips after seeing your code:
Make a simple login JS page and try to get data on form submit URL(use path variable to read it.)
#RequestMapping(path = "/{create}/{user}")
public String createUser(#PathVariable("id") String id, #PathVariable("pass") String pass) {
// read id & pass then save
}
2.Always decode your password and match .i.e both id & password should be matched.
by this you can create as many user you want.

Get current user's extra information by thymeleaf sec tag working with spring security

I'm using thymeleaf-extras-springsecurity4 with spring security on my project. The problem is I cannot get user's extra fields (which means user information on database except username, password, enabled, etc. given by UserDetails) by using <span sec:authentication="principal.something" />.
Heres are my simple codes:
UserEntity (implements UserDetails)
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
#EqualsAndHashCode
#Entity
#Table(name = "users", schema = "myschema")
public class UserEntity implements UserDetails {
#Id
#GeneratedValue
#Column(name = "id", nullable = false)
private int id;
#Basic
#Column(name = "username", nullable = false, unique = true, length = 64)
private String username;
#Basic
#Column(name = "password", nullable = false, columnDefinition = "TEXT")
private String password;
#Basic
#Column(name = "enabled", nullable = false, columnDefinition = "BIT")
private boolean enabled;
#Basic
#Column(name = "phone", nullable = false, length = 16)
private String phone;
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private List<AuthorityEntity> authorities;
#Override
public boolean isAccountNonExpired() {
return enabled;
}
#Override
public boolean isAccountNonLocked() {
return enabled;
}
#Override
public boolean isCredentialsNonExpired() {
return enabled;
}
#Override
public String toString() {
return username;
}
}
AuthorityEntity (implements GrantedAuthority)
#Data
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "authorities", schema = "myschema",
uniqueConstraints = #UniqueConstraint(columnNames = {"user_id", "authority"}))
public class AuthorityEntity implements GrantedAuthority {
#Id
#GeneratedValue
#Column(name = "id", nullable = false)
private int id;
#Basic
#Column(name = "authority", nullable = false, length = 24)
private String authority;
#ManyToOne
#JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false)
private UserEntity user;
}
UserRepository
#Repository
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
UserEntity findOneByUsernameAndEnabledTrue(String username);
}
UserService
#Service
public class UserService {
private UserRepository userRepository;
#Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserEntity loadUserByUsername(String username) {
return userRepository.findOneByUsernameAndEnabledTrue(username);
}
}
SecurityService (extends UserDetailService)
#Service
public class SecurityService implements UserDetailsService {
private UserService userService;
#Autowired
public SecurityService(UserService userService) {
this.userService = userService;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = userService.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return user;
}
}
SecurityConfig (extends WebSecurityConfigurerAdapter)
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private SecurityService securityService;
#Autowired
public SecurityConfig(SecurityService securityService) {
this.securityService = securityService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/user/login").anonymous()
.antMatchers("/**").hasAnyRole("ADMIN", "USER")
.and()
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/")
.and()
.logout()
.logoutUrl("/user/logout")
.logoutSuccessUrl("/")
.and()
.exceptionHandling()
.accessDeniedPage("/error/403");
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
auth.userDetailsService(securityService).passwordEncoder(passwordEncoder);
}
}
index.html (using thymeleaf-extras-springsecurity)
<!DOCTYPE html>
<html lang="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
layout:decorator="layout/base">
<th:block layout:fragment="content">
<h1>Main Page</h1>
<p sec:authentication="principal.username">Username</p>
<p sec:authentication="principal.phone">Phone</p>
</th:block>
The Problem
In index.html, sec:authentication="principal.username" works as expected, but sec:authentication="principal.phone" does not despite my UserDetailsService implementation stores UserEntry which implements UserDetails with extra field phone.
Questions
Is there any way to make sec:authentication="principal.phone" work well? (or "princiapl.getPhone()" respectively)
If not, can I get current user's phone number in my thymeleaf without passing it through controller?
If not, how can I pass current user's UserEntry object without plugging model explicitly for instance through mav of each controller method? Does AOP deal with this?
(Additional) In many other examples applying spring security, they don't implement UserDetails on UserEntry (or similar classes), but make a new UserDetails instance in their UserDetailService implementation like
#Override
public UserDetails loadUserByUsername(String userName)
throws UsernameNotFoundException {
UserInfo activeUserInfo = userInfoDAO.getActiveUser(userName);
GrantedAuthority authority = new SimpleGrantedAuthority(activeUserInfo.getRole());
UserDetails userDetails = (UserDetails)new User(activeUserInfo.getUserName(),
activeUserInfo.getPassword(), Arrays.asList(authority));
return userDetails;
}
(from here). I think my structure is not a good design but I don't know exactly why. Is there any comment for my class design?
Thanks!
If my questions are too vague, let me know so then I would update this more concrete.
In order to use additional fields contained in your user's data in Thymeleaf, you must go through the next steps.
Implement your own Spring Security's user.
Override loadUserByUsername, so that it returns your custom user.
Add the Spring Security's Thymeleaf Extras dependencies.
Use ${#authentication.getPrincipal()}, instead of sec.
STEP 1
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
// Our own implementation of the Spring Security User.
public class MyUser extends User {
// Here we add the extra fields of our users.
private String phone;
private static final long serialVersionUID = 1L;
public MyUser(String username,
String password,
Collection<GrantedAuthority> authorities,
String phone) {
super(username, password, authorities);
this.phone = phone;
}
public String getPhone() {
return realName;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
STEP 2
#Override
public MyUser loadUserByUsername(String userName)
throws AuthenticationException {
// Fetch the user.
UserDetails user = userService.loadUserByUsername(username);
// For each user's authority, add it into our authorities' collection.
Collection<GrantedAuthority> grantedAuthorities = new LinkedList<GrantedAuthority>();
if (user.getAuthorities().size() > 0){
for (Authority authority : user.getAuthorities()) {
// Add a new GrantedAuthority for each user's authorities.
grantedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
}
}
return new MyUser(user.getUsername(), user.getPassword(), grantedAuthorities, user.getPhone());
}
STEP 3
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
STEP 4
<th:block th:with="auth=${#authentication.getPrincipal()}">
<p th:text="${auth ? auth.phone : 'NULL'}">Phone</p>
</th:block>

UserDetailsService config for properly getting user

I create this topic from my previous one Get authenticated user entity Spring MVC where I asked question about properly getting authenticated user entity. I adviced that Principal object (for example, on my view <sec:authentication property="principal.customFieldName" />) can has access to my custom fields if my UserDetailsService configuration is right. Does my UserDetailsService configured properly to accomplish this functionality?
#Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger logger = Logger.getLogger(UserDetailsServiceImpl.class);
#Autowired
#Qualifier("hibernateUserDao")
private UserDAO userDAO;
#Override
#Transactional(readOnly = true)
public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException, DataAccessException {
UserDetails user = userDAO.findByLogin(userName);
if (user == null) {
logger.error("User was not found! Input login: " + userName);
}
return buildUserFormUserEntity(user);
}
#Transactional(readOnly = true)
private org.springframework.security.core.userdetails.User buildUserFormUserEntity(UserDetails userDetails) {
boolean enableStatus = userDetails.isEnabled();
String userName = userDetails.getLogin();
String password = userDetails.getPassword();
boolean enabled = enableStatus;
boolean accountNonExpired = enableStatus;
boolean credentialsNonExpired = enableStatus;
boolean accountNonLocked = enableStatus;
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(userDetails.getRole()));
User springSecurityUser = new User(userName, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
return springSecurityUser;
}
public UserDAO getUserDAO() {
return userDAO;
}
public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}
}
I think you need some additional steps to be able succesfully use
<sec:authentication property="principal.customFieldName" />
on some page:
Add your custom user object that implements org.springframework.security.core.userdetails.UserDetails interface. The simpliest way to do it is to extend existing org.springframework.security.core.userdetails.User class: class CutomUser extends User
Add your customFieldName property to CutomUser class.
Use CutomUser as a return type in your UserDetailsServiceImpl.loadUserByUsername(...) method. Do not forget to fill customFieldName at this moment.

Resources