UserDetailsService身份认证
对于用户流量较大的项目来说,频繁的使用JDBC进行数据库查询认证不仅麻烦,而且会降低网站登录访问速度。对于一个完善的项目来说,如果某些业务已经实现了用户信息查询的服务,就没必要使用JDBC进行身份认证了。
下面假设当前项目中已经有用户信息查询的业务方法,这里,在已有的用户信息服务的基础上选择使用UserDetailsService进行自定义用户身份认证。
1.定义查询用户及角色信息的服务接口
为了案例演示,假设项目中存在一个CustomerService业务处理类,用来通过用户名获取用户及权限信息,内容如文件1所示。
文件1 CustomerService.java
1 import com.itheima.domain.Customer;
2 import com.itheima.domain.Authority;
3 import com.itheima.repository.CustomerRepository;
4 import com.itheima.repository.AuthorityRepository;
5 import org.springframework.beans.factory.annotation.Autowired;
6 import org.springframework.data.redis.core.RedisTemplate;
7 import org.springframework.stereotype.Service;
8 import java.util.List;
9 // 对用户数据结合Redis缓存进行业务处理
10 @Service
11 public class CustomerService {
12 @Autowired
13 private CustomerRepository customerRepository;
14 @Autowired
15 private AuthorityRepository authorityRepository;
16 @Autowired
17 private RedisTemplate redisTemplate;
18 // 业务控制:使用唯一用户名查询用户信息
19 public Customer getCustomer(String username){
20 Customer customer=null;
21 Object o = redisTemplate.opsForValue().get("customer_"+username);
22 if(o!=null){
23 customer=(Customer)o;
24 }else {
25 customer = customerRepository.findByUsername(username);
26 if(customer!=null){
27 redisTemplate.opsForValue().set("customer_"+username,customer);
28 }
29 }
30 return customer;
31 }
32 // 业务控制:使用唯一用户名查询用户权限
33 public List<Authority> getCustomerAuthority(String username){
34 List<Authority> authorities=null;
35 Object o = redisTemplate.opsForValue().get("authorities_"+username);
36 if(o!=null){
37 authorities=(List<Authority>)o;
38 }else {
39 authorities=authorityRepository.findAuthoritiesByUsername(username);
40 if(authorities.size()>0){
41 redisTemplate.opsForValue().set("authorities_"+username,authorities);
42 }
43 }
44 return authorities;
45 }
46 }
文件1所示的业务代码,读者不需要细究,只要明白CustomerService是项目中存在的Customer业务处理类,该类结合Redis缓存定义了通过用户名username获取用户信息和用户权限信息的方法。关于CustomerService业务处理类及其关联的其他代码,可以查阅具体的源代码示例。
2.定义UserDetailsService用于封装认证用户信息
UserDetailsService是Security提供的进行认证用户信息封装的接口,该接口提供的loadUserByUsername(String s)方法用于通过用户名加载用户信息。使用UserDetailsService进行身份认证的时,自定义一个UserDetailsService接口的实现类,通过loadUserByUsername(String s)方法调用用户业务处理类中已有的方法进行用户详情封装,返回一个UserDetails封装类,来供Security认证使用。
下面,自定义一个UserDetailsService接口实现类进行用户认证信息封装,内容如文件2所示。
文件2 UserDetailsServiceImpl.java
1 import com.itheima.domain.Authority;
2 import com.itheima.domain.Customer;
3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.security.core.GrantedAuthority;
5 import org.springframework.security.core.authority.SimpleGrantedAuthority;
6 import org.springframework.security.core.userdetails.*;
7 import org.springframework.stereotype.Service;
8 import java.util.ArrayList;
9 import java.util.List;
10 // 自定义一个UserDetailsService接口实现类进行用户认证信息封装
11 @Service
12 public class UserDetailsServiceImpl implements UserDetailsService {
13 @Autowired
14 private CustomerService customerService;
15 @Override
16 public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
17 // 通过业务方法获取用户及权限信息
18 Customer customer = customerService.getCustomer(s);
19 List<Authority> authorities = customerService.getCustomerAuthority(s);
20 // 对用户权限进行封装
21 List<SimpleGrantedAuthority> list = authorities.stream()
22 .map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
23 .collect(Collectors.toList());
24 // 返回封装的UserDetails用户详情类
25 if(customer!=null){
26 UserDetails userDetails=
27 new User(customer.getUsername(),customer.getPassword(),list);
28 return userDetails;
29 } else {
30 // 如果查询的用户不存在(用户名不存在),必须抛出此异常
31 throw new UsernameNotFoundException("当前用户不存在!");
32 }
33 }
34 }
文件2中,重写了UserDetailsService接口的loadUserByUsername(String s)方法,在该方法中,使用CustomerService业务处理类获取用户的用户信息和权限信息,并通过UserDetails进行认证用户信息封装。
需要注意的是,使用new User()方法封装CustomerService业务处理类获取到的用户时,必须对当前用户进行非空判断,如果查询的用户为空(即用户名错误),需要使用throw关键字抛出UsernameNotFoundException异常;否则,一旦登录的用户名输入错误,程序可能出现Security无法识别的错误。
3.使用UserDetailsService进行身份认证
接下来,在configure(AuthenticationManagerBuilder auth)方法中使用UserDetailsService身份认证的方式进行自定义用户认证,内容如文件3所示。
文件3 SecurityConfig.java
1 import com.itheima.service.UserDetailsServiceImpl;
2 import org.springframework.beans.factory.annotation.Autowired;
3 import org.springframework.security.config.annotation.authentication.builders.*;
4 import org.springframework.security.config.annotation.web.configuration.*;
5 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
6 @EnableWebSecurity // 开启MVC security安全支持
7 public class SecurityConfig extends WebSecurityConfigurerAdapter {
8 @Autowired
9 private UserDetailsServiceImpl userDetailsService;
10 @Override
11 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
12 // 密码需要设置编码器
13 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
14 // 3、使用UserDetailsService进行身份认证
15 auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
16 }
17 }
在文件3中,先通过@Autowired注解引入了自定义的UserDetailsService接口实现类UserDetailsServiceImpl,然后在重写的configure(AuthenticationManagerBuilder auth)方法中使用UserDetailsService身份认证的方式自定义了认证用户信息。在使用UserDetailsService身份认证时,直接调用userDetailsService(T userDetailsService)对UserDetailsServiceImpl实现类进行认证,认证过程中需要对密码进行编码设置。
4.效果测试
重启chapter07项目进行效果测试,项目启动成功后,通过浏览器访问“http:/``/localhost:8080/”访问项目首页(务必保证项目已有的CustomerService业务处理类配置完成,且关联的Redis服务器已经启动),效果如图1所示。
图1 项目首页访问效果
从图1可以看出,执行“http://localhost:8080/
”访问项目首页时,同样自动跳转到了用户登录页面“http://localhost:8080/login
”。此时输入正确或者错误的用户信息。
至此,关于Spring Boot整合Spring Security中的自定义用户认证讲解完毕。内存身份认证最为简单,主要用作测试和新手体验;JDBC身份认证和UserDetailsService身份认证在实际开发中使用较多,而这两种认证方式的选择,主要根据实际开发中已有业务的支持来确定。