版本:

  • springboot2.7

登录

仅仅只是在pom文件中添加springboot security依赖之后,前端就不能再和后端交互了,出现了跨域问题。

想要通过security实现登录,需更改三个地方。

用户校验

security默认使用了InMemoryUserDetailsManager来对用户密码进行校验,它使用的是自己生成的一份用户名和密码,而我们想要其通过用户名从数据库中查询用户的密码,然后和前端发来的密码做匹配。为了完成此功能,需要创建一个继承UserDetailsService接口的类,重写loadUserByUsername方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

@Autowired
private SysUserService userService;

@Autowired
private SysPasswordService passwordService;

@Autowired
private SysPermissionService permissionService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username); //从数据库中获取密码
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("登录用户:" + username + " 不存在");
}
// else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
// {
// log.info("登录用户:{} 已被删除.", username);
// throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
// }
// else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
// {
// log.info("登录用户:{} 已被停用.", username);
// throw new ServiceException("对不起,您的账号:" + username + " 已停用");
// }

passwordService.validate(user);

return createLoginUser(user);
}

public UserDetails createLoginUser(SysUser user)
{
// return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
return new LoginUser(user.getUserId(), user.getDeptId(), user, Collections.emptySet());
}


}

自定义登录接口

为了能够调用loadUserByUsername,需要显示的调用AuthenticationManager类的authenticate方法。authenticate方法接收一个类型为Authentication的参数,而该类型是一个接口,无法创建该类型的对象进行函数传递,一般使用其实现类UsernamePasswordAuthenticationToken。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    @Resource
private AuthenticationManager authenticationManager;
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
// * @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
public String login(String username, String password, String uuid)
{
// // 验证码校验
// validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
// 用户验证
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername(由自己实现),
authentication = authenticationManager.authenticate(authenticationToken);
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{//记录日志,抛出异常
// AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
// AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
}
finally
{
AuthenticationContextHolder.clearContext();
}
// AsyncManager.me().execute(AsyncManagercFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//recordLoginInfo(loginUser.getUserId()); //记用户最近登录信息。更新数据库表sys_user, 更新login_ip和login_date
// 生成token
return tokenService.createToken(loginUser);
}

为了能够自动注入AuthenticationManager类,需要再写一个函数。

1
2
3
4
@Bean
public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

过滤链

security默认会阻止任何未通过认证的请求,需要配置过滤器链,告知在访问哪些路由时不需要验证,比如可以匿名访问登录页面和静态资源。需要创建一个类来对security做一些配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56


@Configuration(proxyBeanMethods = false)
@EnableWebSecurity //to enable Spring Security’s web security support and provide the Spring MVC integration.
public class WebSecurityConfig {

/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;

/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/

//defines which URL paths should be secured and which should not.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable().and()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();

return http.build();
}
}

为了支持对访问其他路由的请求的鉴权认证,需要设置认证过滤器,将根据用户创建的Authentication类型的实例放入到SecurityContextHolder中,以便其他过滤器使用,放行已认证的用户发来的请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 认证过滤器 验证token有效性
*
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;
//封装Authentication, 存入SecurityContextHolder
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{ //需要获取token.如何获取,需要知道前端发来的格式,针对特定格式解析
LoginUser loginUser = tokenService.getLoginUser(request); //获取token, 解析token, 获取userid, 从redis中获取用户信息
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}

将该过滤器添加到过滤器链中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity //to enable Spring Security’s web security support and provide the Spring MVC integration.
public class WebSecurityConfig {


/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;



/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;

/**
* 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
* 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
*/
@Bean
public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/

//defines which URL paths should be secured and which should not.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable().and()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();



// 添加JWT filter
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
}

参考:
Springsecurity2.7+jwt最新版教程 | bilibili