赞
踩
目录
要做一个后台管理系统,会引入多个系统,这就需要做用户认证和权限管理。用户认证通过token来实现,市面上的技术有很多,我这里仅仅来说明一下security+jwt的一种实现过程,没有做页面,需要做页面的同学自行实现。
有些容易入坑的点,我看别的资料没有说太清楚,这里记录下,希望能帮助到跳坑的同学。
- <!-- 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校验两个完全够用
- @Configuration
- @EnableWebSecurity
- @EnableGlobalMethodSecurity(prePostEnabled = true)
- public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
-
- //操作用户
- @Autowired
- FfUserService ffUserService;
-
- //token校验
- @Autowired
- JwtAuthenPreFilter jwtAuthenPreFilter;
-
- /**
- *token异常
- */
- @Autowired
- UnauthorizedHandler unauthorizedHandler;
-
- //配置放行策略
- @Value("${jwt.security.antMatchers}")
- private String antMatchers;
-
-
- /**
- * 密码加密算法
- *
- * @return
- */
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder(10);
- }
-
- /**
- * 验证用户来源,主要是验证账号和密码
- *
- * @param auth
- * @throws Exception
- */
- @Override
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- auth.userDetailsService(ffUserService).passwordEncoder(passwordEncoder());
- }
-
- /**
- * 忽略策略
- * @param web
- * @throws Exception
- */
- @Override
- public void configure(WebSecurity web) throws Exception {
- web.ignoring().antMatchers(antMatchers.split(","));
- }
-
- /**
- * 用户授权
- * @param http
- * @throws Exception
- */
- @Override
- protected 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: 添加一个过滤器在指定过滤器位置
- @Component
- @Slf4j
- public class UnauthorizedHandler implements AuthenticationEntryPoint {
- @Override
- public 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异常就不美观了,也可以完善这个类。
- 也可以扩展单独的异常处理模块做统一处理,但是业务异常我还是推荐根据业务场景来单独处理,一味的追求统一处理不见得都是好事。
- @Component
- @Slf4j
- public class JwtAuthenPreFilter extends OncePerRequestFilter {
-
- @Autowired
- private 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;
- @Autowired
- private FfUserService ffUserService ;
-
- public JwtAuthenPreFilter() {
- }
-
- @Override
- protected 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验证的url
- SkipPathAntMatcher 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 {
- //需要校验却无用户token
- log.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();
- }
- }
- }
- @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;
- }
- @Override
- public 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);
- }
- }
- @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();
- }
-
- }
- @Slf4j
- public class SkipPathAntMatcher implements RequestMatcher {
- private List<String> pathsToSkip;
-
- public SkipPathAntMatcher(List<String> pathsToSkip) {
- this.pathsToSkip = pathsToSkip;
- }
-
- @Override
- public 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】,我拉大家进我的读者交流群。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。