一、配置
相关的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 相关工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.5.7</version>
</dependency>
配置类
为SpringSecurity添加一个配置类:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf
.disable()
.sessionManagement()// 基于token,所以不需要session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, // 允许对于网站静态资源的无授权访问
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources/**",
"/v2/api-docs/**"
)
.permitAll()
.antMatchers("/user/login", "/user/register")// 对登录注册要允许匿名访问
.permitAll()
.antMatchers(HttpMethod.OPTIONS) // 允许进行options请求
.permitAll()
// .antMatchers("/**") // 测试时全部运行访问
// .permitAll()
.anyRequest() // 除上面外的所有请求全部需要鉴权认证
.authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
}
}
configure(HttpSecurity httpSecurity)
用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器。
上面有一处注释 antMatchers("/**")
,放开的话,所有的接口路径都可以通过验证。
如果指定匹配,则只有指定的路径不需要验证,比如访问/user/login
可以不需要验证。
除此之外的路径则需要进行授权,比如/user/msg
:
{
"timestamp": "2020-03-25T08:44:34.629+0000",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/user/msg"
}
二、添加自定义未授权时的返回
在SpringSecurity配置中添加自定义未授权和未登录结果返回:
import com.example.test.component.RestAuthenticationEntryPoint;
import com.example.test.component.RestfulAccessDeniedHandler;
...
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
...
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}
}
这里需要 RestfulAccessDeniedHandler
和 RestAuthenticationEntryPoint
两个类处理403和401两种错误类型
RestfulAccessDeniedHandler
当用户没有访问权限时的处理器,用于返回JSON格式的处理结果;RestAuthenticationEntryPoint
当未登录或token失效时,返回JSON格式的结果;
import cn.hutool.json.JSONUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import top.xiaoyulive.common.api.CommonResult;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 当访问接口没有权限时,自定义的返回结果
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e)
throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
response.getWriter().flush();
}
}
import cn.hutool.json.JSONUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import top.xiaoyulive.common.api.CommonResult;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 当未登录或者token失效访问接口时,自定义的返回结果
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
response.getWriter().flush();
}
}
三、结合JWT进行验证
在SpringSecurity配置中添加过滤器:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
...
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
}
}
需要创建一个过滤器 JwtAuthenticationTokenFilter
,在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import top.xiaoyulive.common.util.JwtTokenUtil;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT登录授权过滤器
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
private static final String TOKEN_HEADER = "Authorization"; // JWT存储的请求头
private static final String TOKEN_HEAD = "Bearer"; // JWT负载中拿到开头
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String authHeader = request.getHeader(TOKEN_HEADER);
if (authHeader != null && authHeader.startsWith(TOKEN_HEAD)) {
String authToken = authHeader.substring(TOKEN_HEAD.length()).trim(); // The part after "Bearer "
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("authenticated user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
UserDetails
:SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现,后面详细说到。
四、JwtTokenUtil工具类
我们还需要一个JwtTokenUtil工具类,用于简化jwt的操作:
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成的工具类
* JWT token的格式:header.payload.signature
* header的格式(算法、token的类型):{"alg": "HS512","typ": "JWT"}
* payload的格式(用户名、创建时间、生成时间):{"sub":"wang","created":1489079981393,"exp":1489684781}
* signature的生成算法:HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
*/
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 根据负责生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}", token);
}
return claims;
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 当原来的token没过期时是可以刷新的
*
* @param oldToken 带tokenHead的token
*/
public String refreshHeadToken(String oldToken) {
if(StrUtil.isEmpty(oldToken)){
return null;
}
String token = oldToken.substring(tokenHead.length());
if(StrUtil.isEmpty(token)){
return null;
}
//token校验不通过
Claims claims = getClaimsFromToken(token);
if(claims==null){
return null;
}
//如果token已经过期,不支持刷新
if(isTokenExpired(token)){
return null;
}
//如果token在30分钟之内刚刷新过,返回原token
if(tokenRefreshJustBefore(token,30*60)){
return token;
}else{
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
/**
* 判断token在指定时间内是否刚刚刷新过
* @param token 原token
* @param time 指定时间(秒)
*/
private boolean tokenRefreshJustBefore(String token, int time) {
Claims claims = getClaimsFromToken(token);
Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
Date refreshDate = new Date();
//刷新时间在创建时间的指定时间内
if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){
return true;
}
return false;
}
}
其中用到配置:
jwt:
tokenHeader: Authorization #JWT存储的请求头
secret: admin-secret #JWT加解密使用的密钥
expiration: 604800 #JWT的超期限时间(60*60*24)
tokenHead: Bearer #JWT负载中拿到开头
五、用户表结构
为了实现效果,我们还需要创建一张用户表,并为其创建模型、Dao、Mapper、Service等。
首先先看看User模型结构:
@Alias("user")
@Data
public class User implements Serializable {
private Long id;
private String username;
private String password;
private String nickname;
private String phone;
private Integer status;
private String icon;
private Integer gender;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
private static final long serialVersionUID = 1L;
}
再放出Service层的实现代码,至于Dao层自己补充了,都是非常明显的一些方法:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void create(User user) {
userDao.create(user);
}
@Override
public void delete(Long id) {
userDao.delete(id);
}
@Override
public void update(User user) {
userDao.update(user);
}
@Override
public User query(Long id) {
return userDao.query(id);
}
@Override
public User findByUsername(String username) {
return userDao.findByUsername(username);
}
@Override
public List<User> list() {
return userDao.list();
}
}
六、实现UserDetails
创建一个UserDetailsDto,实现UserDetails接口:
package com.example.test.dto;
import com.example.test.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class UserDetailsDto implements UserDetails {
private User user;
public UserDetailsDto(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 之后添加
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.getStatus().equals(1);
}
}
七、注册的处理
修改SpringSecurity的配置:
package com.example.test.configuration;
import com.example.test.dto.UserDetailsDto;
import com.example.test.filter.JwtAuthenticationTokenFilter;
import com.example.test.model.User;
import com.example.test.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
...
}
// 配置userDetailsService和passwordEncoder
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
// SpringSecurity定义的用于对密码进行编码及比对的接口
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 获取登录用户信息
@Bean
public UserDetailsService userDetailsService() {
return username -> {
User user = userService.findByUsername(username);
if (user != null) {
return new UserDetailsDto(user);
}
throw new UsernameNotFoundException("用户名或密码错误");
};
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
configure(AuthenticationManagerBuilder auth)
用于配置UserDetailsService及PasswordEncoder;PasswordEncoder
SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder;UserDetailsService
SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现;
添加注册接口:
@RestController
@RequestMapping(value="/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("register")
public Object register(@RequestBody User user) {
User userNew = new User();
// 查询是否有相同用户名的用户
User userOld = userService.findByUsername(user.getUsername());
if (userOld != null) {
return null;
}
// 对新增用户做一些处理
BeanUtils.copyProperties(user, userNew);
userNew.setCreateTime(new Date());
userNew.setStatus(1);
// 将密码进行加密操作
String encodePassword = passwordEncoder.encode(user.getPassword());
userNew.setPassword(encodePassword);
userService.create(userNew);
return userNew;
}
}
密码使用了 passwordEncoder.encode
处理,处理后的密码大概长这样:
$2a$10$GXSA2sf0hP73BF1niznT8u1llABwuVi3op3BDFABibCB3hXBywcXa
八、登录的处理
在控制器中添加登录的接口:
package com.example.test.controller;
import com.example.test.common.api.CommonResult;
import com.example.test.common.utils.JwtTokenUtil;
import com.example.test.model.User;
import com.example.test.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping(value="/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
// 登录
@PostMapping("login")
public Object login(@RequestBody User user){
String token = null;
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
// 取出密码,并验证密码是否与数据库中加密的密码匹配
if (!passwordEncoder.matches(user.getPassword(), userDetails.getPassword())) {
throw new BadCredentialsException("密码不正确");
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
token = jwtTokenUtil.generateToken(userDetails);
} catch (AuthenticationException e) {
LOGGER.warn("登录异常:{}", e.getMessage());
}
if (token == null) {
return CommonResult.validateFailed("用户名或密码错误");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", "Bearer");
return CommonResult.success(tokenMap);
}
}
访问 /user/login
,如果用户名与密码正确,将返回如下格式的数据:
{
"code": 200,
"message": "操作成功",
"data": {
"tokenHead": "Bearer",
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ4aWFveXUiLCJjcmVhdGVkIjoxNTg1MTg2NDc1MzI4LCJleHAiOjE1ODU3OTEyNzV9.vJIsNP2UAR0dwdbuIn8ggmcMmZ0asFJkWvoB4Mzj6LwlidQT1U-TqaL93slgUqW05wLfprvGPsRXjm_gntcysw"
}
}
九、普通接口访问
普通接口需要携带token才能正常访问
请求头key:Authorization
请求头value举例:Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ4aWFveXUiLCJjcmVhdGVkIjoxNTg1MTg2NDc1MzI4LCJleHAiOjE1ODU3OTEyNzV9.vJIsNP2UAR0dwdbuIn8ggmcMmZ0asFJkWvoB4Mzj6LwlidQT1U-TqaL93slgUqW05wLfprvGPsRXjm_gntcysw
@RestController
@RequestMapping(value="/user")
public class UserController {
// 普通接口
@GetMapping("/msg")
public String getMsg(){
return "你已通过验证";
}
}
如果携带token访问,将正确返回数据,以 /user/msg
为例
如果不携带token,则报401错误:
十、Swagger文档支持
如果想要在Swagger文档中支持Authorization请求头,需要修改Swagger配置:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
// 为当前包下controller生成API文档
.apis(RequestHandlerSelectors.basePackage("com.test.example.controller"))
.paths(PathSelectors.any())
.build()
// 添加登录认证
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("SwaggerUI演示")
.description("test")
.contact("xiaoyu")
.version("1.0")
.build();
}
private List<ApiKey> securitySchemes() {
// 设置请求头信息
List<ApiKey> result = new ArrayList<>();
ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header");
result.add(apiKey);
return result;
}
private List<SecurityContext> securityContexts() {
// 设置需要登录认证的路径
List<SecurityContext> result = new ArrayList<>();
result.add(getContextByPath("/brand/.*"));
return result;
}
private SecurityContext getContextByPath(String pathRegex){
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex))
.build();
}
private List<SecurityReference> defaultAuth() {
List<SecurityReference> result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
result.add(new SecurityReference("Authorization", authorizationScopes));
return result;
}
}
十一、拆分授权模块以共用
我们可以将认证模块拆解出来,以提供其他模块使用。
思路如下:将授权模块拆解为公共模块,其他模块引入,配置不需要进行授权验证的路径列表(路径白名单),加载进自己的userDetailsService。
授权公共模块
- 配置不需要授权验证的资源路径(路径白名单) ```java import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource;
import java.util.ArrayList; import java.util.List;
@Data
@Configuration
@PropertySource(“classpath:application.yaml”) //指明配置源文件位置
@ConfigurationProperties(prefix = “secure.ignored”)
@EnableConfigurationProperties(IgnoreUrlsConfig.class)
public class IgnoreUrlsConfig {
private List
2. 对SpringSecurity的配置的扩展,支持自定义白名单资源路径和查询用户逻辑
```java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import top.xiaoyulive.common.component.RestAuthenticationEntryPoint;
import top.xiaoyulive.common.component.RestfulAccessDeniedHandler;
import top.xiaoyulive.common.filter.JwtAuthenticationTokenFilter;
import top.xiaoyulive.common.util.JwtTokenUtil;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
// 不需要保护的资源路径允许访问
for (String url : ignoreUrlsConfig().getUrls()) {
registry.antMatchers(url).permitAll();
}
// 允许跨域请求的OPTIONS请求
registry.antMatchers(HttpMethod.OPTIONS)
.permitAll();
// 任何请求需要身份认证
registry.and()
.authorizeRequests()
.anyRequest()
.authenticated()
// 关闭跨站请求防护及不使用session
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 自定义权限拒绝处理类
.and()
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler())
.authenticationEntryPoint(restAuthenticationEntryPoint())
// 自定义权限拦截器JWT过滤器
.and()
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
return new RestfulAccessDeniedHandler();
}
@Bean
public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
return new RestAuthenticationEntryPoint();
}
@Bean
public IgnoreUrlsConfig ignoreUrlsConfig() {
return new IgnoreUrlsConfig();
}
@Bean
public JwtTokenUtil jwtTokenUtil() {
return new JwtTokenUtil();
}
}
其他模块引入
在其他需要授权认证的模块,引入授权公共模块
<dependency> <groupId>org.example</groupId> <artifactId>common-security</artifactId> <version>1.0-SNAPSHOT</version> <scope>compile</scope> </dependency>
配置路径白名单(不需要走授权的路径)
secure: ignored: urls: #安全路径白名单 - /swagger-ui.html - /swagger-resources/** - /swagger/** - /**/v2/api-docs - /**/*.js - /**/*.css - /**/*.png - /**/*.ico - /admin/login - /admin/register - /admin/logout - /test/test
添加模块自己的授权配置类,继承SecurityConfig ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.UserDetailsService; import top.xiaoyulive.common.component.DynamicSecurityService; import top.xiaoyulive.common.config.SecurityConfig; import top.xiaoyulive.web.model.UmsResource; import top.xiaoyulive.web.service.UmsAdminService; import top.xiaoyulive.web.service.UmsResourceService;
import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled=true) public class WebSecurityConfig extends SecurityConfig { @Autowired private UserService userService;
@Bean
public UserDetailsService userDetailsService() {
return username -> {
User user = userService.findByUsername(username);
if (user != null) {
return new UserDetailsDto(user);
}
throw new UsernameNotFoundException("用户名或密码错误");
};
}
} ```
控制器、Service等跟之前的一致即可。