这是我参与更文挑战的第7天,活动详情查看: 更文挑战
前言
上文简单描述了什么是SpringSecurity,本文主要讲述如何在SpringBoot中集成Spring Security
往期链接
一、开发环境
开发环境如下所示:
- Java:OpenJDK 11
- Spring Security:5.3.3
二、集成Spring Security
此处以SpringSecurity + JWT为例
1.引入依赖
Spring-security无需指定版本,在父pom中已指定。因需要JWT所以也需要引入JJWT库
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JJWT是一个提供端到端的JWT创建和验证的Java库 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
2.配置Spring Security
主配置
主配置中主要声明了需要拦截的请求以及过滤器Filter的定义
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/user/register")
.antMatchers("/doc.html")
.antMatchers("/webjars/**")
.antMatchers("/swagger-resources/**")
.antMatchers("/v2/**")
.antMatchers("/favicon.ico")
// 微信获取openId的Url不拦截
.antMatchers("/wx/openId/*")
.antMatchers("/wx/login")
// 登录二维码不拦截
.antMatchers("/qrcode/**");
web.expressionHandler(new DefaultWebSecurityExpressionHandler() {
@Override
protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {
WebSecurityExpressionRoot root = (WebSecurityExpressionRoot) super.createSecurityExpressionRoot(authentication, fi);
root.setDefaultRolePrefix(""); // remove the prefix ROLE_
return root;
}
});
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
return req -> {
CorsConfiguration cfg = new CorsConfiguration();
cfg.addAllowedHeader("*");
cfg.addAllowedMethod("*");
cfg.addAllowedOrigin("*");
cfg.setAllowCredentials(true);
cfg.checkOrigin("*");
return cfg;
};
}
}
身份验证
身份验证过滤器中主要就是从请求中获取用户名以及密码,交给AuthenticationManager来处理
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/user/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 从输入流中获取到登录的信息
try {
LoginVO loginVO = new ObjectMapper().readValue(request.getInputStream(), LoginVO.class);
return authenticationManager.authenticate(
// 此处需要将密码加密验证
new UsernamePasswordAuthenticationToken(loginVO.getUsername(), loginVO.getPassword())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* 成功调用的方法
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
JWTUser jwtUser = (JWTUser) authResult.getPrincipal();
log.info("JWTUser:{}",jwtUser.toString());
List<String> permissionList = new ArrayList<>();
jwtUser.getAuthorities().forEach(n -> permissionList.add(n.getAuthority()));
// 通过获取Spring上下文来获取JWTConfig对象
JWTConfig jwtConfig = SpringContextUtil.getBean(JWTConfig.class);
String token = jwtConfig.createToken(jwtUser.getId().toString(),permissionList);
UserService userService = SpringContextUtil.getBean(UserService.class);
UserDO userDO = userService.getById(jwtUser.getId());
LoginSuccessVO vo = new LoginSuccessVO();
BeanUtils.copyProperties(userDO, vo);
vo.setToken(token);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSON.toJSONString(Result.success(vo)));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSON.toJSONString(Result.error(CodeMsg.LOGIN_ERROR), SerializerFeature.WriteMapNullValue));
}
}
授权
授权过滤器主要就做一件事情:设置SecurityContext的上下文中的Authentication
@Slf4j
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader("token");
// 校验token正确性
try {
checkToken(token);
// 如果请求头中有token,则进行解析,并且设置认证信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(token));
super.doFilter(request, response, chain);
} catch (BaseException e) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSON.toJSONString(Result.error(e.getCodeMsg(), e.getMessage()), SerializerFeature.WriteMapNullValue));
}
}
// 这里从token中获取用户信息并新建一个token
private UsernamePasswordAuthenticationToken getAuthentication(String token) {
JWTConfig jwtConfig = SpringContextUtil.getBean(JWTConfig.class);
Claims claims = jwtConfig.getTokenClaim(token);
if (claims != null){
String userId = jwtConfig.getUserIdFromToken(claims);
// 设置当前用户Id到线程中
LocalUserId.set(Long.valueOf(userId));
List<String> permissions = jwtConfig.getPermissions(claims);
List<GrantedAuthority> list = new ArrayList<>();
permissions.forEach(
n-> list.add(new SimpleGrantedAuthority(n))
);
return new UsernamePasswordAuthenticationToken(userId, null, list);
}else {
log.error("token格式不正确");
throw new BusinessException(CodeMsg.JWT_EXCEPTION);
}
}
/**
* 校验token的正确性
*/
private void checkToken(String token) {
JWTConfig jwtConfig = SpringContextUtil.getBean(JWTConfig.class);
if(StringUtils.isEmpty(token)){
log.error("{}不能为空",jwtConfig.getHeader());
throw new BusinessException(CodeMsg.JWT_EXCEPTION);
}
Claims claims = jwtConfig.getTokenClaim(token);
if(claims == null){
log.error(CodeMsg.JWT_EXCEPTION.getMessage());
throw new BusinessException(CodeMsg.JWT_EXCEPTION);
}
if (jwtConfig.isTokenExpired(claims)) {
log.error(CodeMsg.TOKEN_EXPIRED.getMessage());
throw new ParameterException(CodeMsg.TOKEN_EXPIRED);
}
}
}
3.权限接口编写
此处以记账分析中的某个接口为例
@PreAuthorize("hasAuthority('record:analysis:spendCategoryTotal')")代表当前用户需拥有此权限字符
@LoginRequired
@CommonLog(title = "获取某年所有消费类别的总额", businessType = BusinessType.QUERY)
@ApiOperation(value = "获取某年所有消费类别的总额")
@PreAuthorize("hasAuthority('record:analysis:spendCategoryTotal')")
@GetMapping("/spendCategoryTotal/{year}/{recordType}")
public Result<?> getSpendCategoryTotalInYear(@ApiParam(required = true, value = "年(yyyy)") @Validated
@DateTimeFormat(pattern="yyyy") @PathVariable(value = "year") Date date,
@NotNull(message = "记账类型编码不能为空") @PathVariable(value = "recordType")String recordType) {
if (!recordType.equals(RecordConstant.EXPEND_RECORD_TYPE) && !recordType.equals(RecordConstant.INCOME_RECORD_TYPE)) {
return Result.error(CodeMsg.RECORD_TYPE_CODE_ERROR);
}
UserDO userDO = LocalUser.get();
List<SpendCategoryTotalDTO> list = recordDetailService.getSpendSpendCategoryTotalByYear(userDO.getId(), recordType,
date);
return Result.success(list);
}
4.测试
可以成功获取到结果
三、总结
以上代码均可在简账后端中找到
感谢看到最后,非常荣幸能够帮助到你~♥