目录
一、背景
二、基本jar依赖引入
三、security模块
1、编写配置类
3、security验证用户名和密码的部分
四、jwt模块
1、jwt原理部分
2、jwt一共需要四个类
五、总结
一、背景
要做一个后台管理系统,会引入多个系统,这就需要做用户认证和权限管理。用户认证通过token来实现,市面上的技术有很多,我这里仅仅来说明一下security+jwt的一种实现过程,没有做页面,需要做页面的同学自行实现。
有些容易入坑的点,我看别的资料没有说太清楚,这里记录下,希望能帮助到跳坑的同学。
二、基本jar依赖引入
<!-- security -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency><!-- jwt依赖 -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>
总结:
我看有的帖子也引入了jjwt的API、impl包,我这里没有用到,实现权限控制和token校验两个完全够用
三、security模块
1、编写配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {//操作用户@AutowiredFfUserService ffUserService;//token校验@AutowiredJwtAuthenPreFilter jwtAuthenPreFilter;/***token异常*/@AutowiredUnauthorizedHandler unauthorizedHandler;//配置放行策略@Value("${jwt.security.antMatchers}")private String antMatchers;/*** 密码加密算法** @return*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(10);}/*** 验证用户来源,主要是验证账号和密码** @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(ffUserService).passwordEncoder(passwordEncoder());}/*** 忽略策略* @param web* @throws Exception*/@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers(antMatchers.split(","));}/*** 用户授权* @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated() // 所有的验证都需要验证.and().csrf().disable() // 禁用 Spring Security 自带的跨域处理
// 定制我们自己的 session 策略:调整为让 Spring Security 不创建和使用 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().exceptionHandling().authenticationEntryPoint(unauthorizedHandler);// 将自定义的过滤器添加在指定过滤器之前http.addFilterBefore(jwtAuthenPreFilter, FilterSecurityInterceptor.class);// 禁用缓存http.headers().cacheControl();}}
总结:
- 需要放行的可以在两个地方配置,第一种如上图;第二种可以在第二个configure中配置。比如:.antMatchers(antMatchers.split(",")).permitAll()
- 在第二个configure中这里特别注意一下配置的顺序,exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint())如果放在前面会导致放行策略不生效。
- security是通过用户名和密码来实现认证的,不一定能满足实际业务需要,所以要扩展,前后端分离目前常用的做法就是基于usertoken的,即上面的自定义的jwtAuthenPreFilter过滤器
- addFilterAfter: 将自定义的过滤器添加在指定过滤器之后
- addFilterBefore:将自定义的过滤器添加在指定过滤器之前
- addFilter:添加一个过滤器,但必须是Spring Security自身提供的过滤器实例或其子过滤器
- addFilterAt: 添加一个过滤器在指定过滤器位置
2、UnauthorizedHandler代码
@Component
@Slf4j
public class UnauthorizedHandler implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {//用户登录时身份认证未通过if(e instanceof BadCredentialsException){//用户登录时身份认证失败ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.TOKEN_INVALID.getCode(), e.getMessage());}else if (e instanceof InsufficientAuthenticationException){//缺少请求头参数,Authorization传递是token值,所以是参数是必须的ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.NO_TOKEN.getCode(), ErrorCodeEnum.NO_TOKEN.getMessage());}else{//用户token无效ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.TOKEN_INVALID.getCode(), ErrorCodeEnum.TOKEN_INVALID.getMessage());}}
}
注意:
- 接口中的逻辑异常要捕获,不然会被拦截报token异常就不美观了,也可以完善这个类。
- 也可以扩展单独的异常处理模块做统一处理,但是业务异常我还是推荐根据业务场景来单独处理,一味的追求统一处理不见得都是好事。
3、security验证用户名和密码的部分
- 网上资料很多,大家自己补充
四、jwt模块
1、jwt原理部分
- 网上资料很多,大家自己补充
2、jwt一共需要四个类
- JwtAuthenPreFilter:token校验和有关业务,这部分可以根据自己项目来实现
@Component
@Slf4j
public class JwtAuthenPreFilter extends OncePerRequestFilter {@Autowiredprivate JwtTokenUtil jwtTokenUtil;//@Autowired//private RedisUtil redisUtil;/*** 防止filter被执行两次*/private static final String FILTER_APPLIED = "__spring_security_JwtAuthenPreFilter_filterApplied";@Value("${jwt.header:Authorization}")private String tokenHeader;@Value("${jwt.tokenHead:Bearer}")private String tokenHead;/*** 距离快过期多久刷新令牌*/@Value("${jwt.token.subRefresh:#{10*60}}")private Long subRefresh;// 不需要认证的接口@Value("${jwt.security.antMatchers}")private String antMatchers;@Autowiredprivate FfUserService ffUserService ;public JwtAuthenPreFilter() {}@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {if (httpServletRequest.getAttribute(FILTER_APPLIED) != null) {filterChain.doFilter(httpServletRequest, httpServletResponse);return;}httpServletRequest.setAttribute(FILTER_APPLIED, true);//过滤掉不需要token验证的urlSkipPathAntMatcher skipPathRequestMatcher = new SkipPathAntMatcher(Arrays.asList(antMatchers.split(",")));if (skipPathRequestMatcher.matches(httpServletRequest)) {filterChain.doFilter(httpServletRequest, httpServletResponse);} else {try {//1.判断是否有效 2.判断是否过期 3.如果未过期的,且过期时间小于10分钟的延长过期时间,并在当前response返回新的header,客户端需替换此令牌String authHeader = httpServletRequest.getHeader(this.tokenHeader);if (authHeader != null && authHeader.startsWith(tokenHead)) {final String authToken = authHeader.substring(tokenHead.length());JWTUserDetail userDetail = jwtTokenUtil.getUserFromToken(authToken);if (ObjectUtils.isEmpty(userDetail)) {log.info("令牌非法,解析失败{}!", authToken);throw new BadCredentialsException(ErrorCodeEnum.TOKEN_INVALID.getMessage());}if (jwtTokenUtil.isTokenExpired(authToken)) {log.info("令牌已失效!{}", authToken);throw new BadCredentialsException(ErrorCodeEnum.TOKEN_INVALID.getMessage());}//令牌快过期生成新的令牌并设置到返回头中,客户端在每次的restful请求如果发现有就替换原值if (new Date(System.currentTimeMillis() - subRefresh).after(jwtTokenUtil.getExpirationDateFromToken(authToken))) {String resAuthToken = jwtTokenUtil.generateToken(userDetail);httpServletResponse.setHeader(tokenHeader, tokenHead + resAuthToken);}JwtTokenUtil.LOCAL_USER.set(userDetail);UserDetails userDetails = ffUserService.loadUserByUsername(userDetail.getLoginName());UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));SecurityContextHolder.getContext().setAuthentication(authentication);} else {//需要校验却无用户tokenlog.info("无header请求-->" + httpServletRequest.getRequestURI());throw new InsufficientAuthenticationException(ErrorCodeEnum.NO_TOKEN.getMessage());}} catch (Exception e) {//log.info("令牌解析失败!", e);throw new BadCredentialsException(ErrorCodeEnum.TOKEN_INVALID.getMessage());}filterChain.doFilter(httpServletRequest, httpServletResponse);//调用完成后清除JwtTokenUtil.LOCAL_USER.remove();}}
}
- JWTUserDetail:token转换成user类
@Data
public class JWTUserDetail implements Serializable {/*** 登陆用户编号*/private long userId;/*** 登陆用户账户名称(可能为手机号邮箱或者名称用户维度唯一)*/private String loginName;/*** 登陆用户类型*/private UserType userType;/*** 登陆用户凭证*/private String jwtToken;/*** 登陆时间*/private Date loginTime;private static ObjectMapper mapper = new ObjectMapper();public enum UserType {User("USER", 1),Operator("OPT", 2),Erp("ERP", 3);private String name;private int index;private UserType(String name, int index) {this.name = name;this.index = index;}@Overridepublic String toString() {return this.name;}public static String getName(int index) {for (UserType c : UserType.values()) {if (c.getIndex() == index) {return c.getName();}}return null;}public String getName() {return name;}public int getIndex() {return index;}}public static JWTUserDetail fromJson(String json) throws JsonProcessingException {return mapper.readValue(json,JWTUserDetail.class);//JSONObject.parseObject(json, JWTUserDetail.class);}public String toJson() throws JsonProcessingException {return mapper.writeValueAsString(this);//.toJSONString(this);}
}
- JwtTokenUtil:token工具
@Component
public class JwtTokenUtil implements Serializable {private static final long serialVersionUID = -5883980282405596071L;public static final ThreadLocal<JWTUserDetail> LOCAL_USER = new ThreadLocal<>();public final static String JWT_TOKEN_PREFIX = "jwt:%s:%d";private final String JWT_LOGIN_NAME = "JWT_LOGIN_NAME";private final String JWT_LOGIN_TIME = "JWT_LOGIN_TIME";private final String JWT_LOGIN_USERID = "JWT_LOGIN_USERID";private final String JWT_LOGIN_USERTYPE = "JWT_LOGIN_USERTYPE";//签名方式private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;//密匙@Value("${jwt.security.secret}")private String secret;@Value("${jwt.access_token:#{30*24*60*60}}")private Long access_token_expiration;public String getLoginNameFromToken(String token) {return getClaimsFromToken(token).getSubject();}/*** 根据token 获取用户信息*/public JWTUserDetail getUserFromToken(String token) {JWTUserDetail jwtUserDetails = new JWTUserDetail();Claims claims = getClaimsFromToken(token);jwtUserDetails.setUserId(claims.get(JWT_LOGIN_USERID, Long.class));jwtUserDetails.setLoginName(claims.get(JWT_LOGIN_NAME, String.class));jwtUserDetails.setUserType(Enum.valueOf(JWTUserDetail.UserType.class, (String) claims.get(JWT_LOGIN_USERTYPE)));jwtUserDetails.setLoginTime(new Date(claims.get(JWT_LOGIN_TIME, Long.class)));jwtUserDetails.setJwtToken(token);return jwtUserDetails;}/*** 根据用户信息生成token** @param user* @return*/public String generateToken(JWTUserDetail user) {Map<String, Object> claims = new HashMap<>();claims.put(JWT_LOGIN_NAME, user.getLoginName());claims.put(JWT_LOGIN_TIME, user.getLoginTime());claims.put(JWT_LOGIN_USERID, user.getUserId());claims.put(JWT_LOGIN_USERTYPE, user.getUserType());return Jwts.builder()//一个map 可以资源存放东西进去.setClaims(claims)// 用户名写入标题.setSubject(user.getLoginName()).setId(UUID.randomUUID().toString()).setIssuedAt(new Date())//过期时间.setExpiration(new Date(System.currentTimeMillis() + access_token_expiration * 1000))//数字签名.signWith(SIGNATURE_ALGORITHM, secret).compact();}/*** 根据token 获取生成时间*/public Date getCreatedDateFromToken(String token) {return getClaimsFromToken(token).getIssuedAt();}/*** 根据token 获取过期时间*/public Date getExpirationDateFromToken(String token) {return getClaimsFromToken(token).getExpiration();}/*** token 是否过期*/public Boolean isTokenExpired(String token) {return getExpirationDateFromToken(token).before(new Date());}/**** 解析token 信息* @param token* @return*/private Claims getClaimsFromToken(String token) {return Jwts.parser()//签名的key.setSigningKey(secret)// 签名token.parseClaimsJws(token).getBody();}}
- SkipPathAntMatcher:token校验的放行策略
@Slf4j
public class SkipPathAntMatcher implements RequestMatcher {private List<String> pathsToSkip;public SkipPathAntMatcher(List<String> pathsToSkip) {this.pathsToSkip = pathsToSkip;}@Overridepublic boolean matches(HttpServletRequest request) {if (!ObjectUtils.isEmpty(pathsToSkip)) {for (String s : pathsToSkip) {AntPathRequestMatcher antPathRequestMatcher = new AntPathRequestMatcher(s);if (antPathRequestMatcher.matches(request)) {return true;}}}return false;}
}
五、总结
- 放行策略有两个地方一定要都配置,1.security要配置,2.token的过滤器也要配置。
- security是校验URL是否有权限访问或者直接放行,我这里没有写用户角色和权限,是因为后面我计划给不同的角色返回不同的菜单,通过菜单来区分。如果你的业务需要可以单独设置
- token过滤器是配置这次请求是否token有效。security放行的,token也要放行。比如登录和注册页面,这时用户没有任何权限的,所以都放行。
最后,欢迎大家关注我的个人公众号,我会把经历分享出来,助你了解圈内圈外事。

同时也欢迎大家添加个人微信【shishuai860505】,我拉大家进我的读者交流群。




















