springsecurity-jwt整合
  MV69bd6votD4 2023年11月02日 57 0

2、springsecurity-jwt整合

2.1整合springsecurity

1)

<dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-security</artifactId>
 </dependency>

2.2认证授权流程

springsecurity-jwt整合_spring

认证管理

springsecurity-jwt整合_java_02

流程图解读:

1、用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

2、然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。

3、认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。

4、SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。

授权管理

springsecurity-jwt整合_java_03

访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。

2.3编写自己的UserDetails和UserDetailService

2.3.1UserDetails
package com.ds.book.entity;
 
 import com.baomidou.mybatisplus.annotation.TableName;
 import java.io.Serializable;
 import java.util.Collection;
 
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.experimental.Accessors;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 
 /**
  * <p>
  * 
  * </p>
  *
  * @author java大师
  * @since 2023-03-17
  */
 @Data
 @EqualsAndHashCode(callSuper = false)
 @Accessors(chain = true)
 @TableName("t_user")
 public class User implements Serializable, UserDetails {
 
     private static final long serialVersionUID = 1L;
     
     private Integer id;
 
     /**
      * 登录名
      */
     private String name;
 
     /**
      * 用户名
      */
     private String username;
 
     /**
      * 密码
      */
     private String password;
 
     /**
      * 是否有效:1-有效;0-无效
      */
     private String status;
 
 
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {
         return roles
                 .stream()
                 .map(role -> new SimpleGrantedAuthority(role.getRoleCode()))
                 .collect(Collectors.toList());
     }
 
     @Override
     public boolean isAccountNonExpired() {
         return true;
     }
 
     @Override
     public boolean isAccountNonLocked() {
         return true;
     }
 
     @Override
     public boolean isCredentialsNonExpired() {
         return true;
     }
 
     @Override
     public boolean isEnabled() {
         return true;
     }
 }
2.3.2userDetailService

登录成功后,将UserDetails的roles设置到用户中

package com.ds.book.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.ds.book.entity.User;
 import com.ds.book.mapper.UserMapper;
 import com.ds.book.service.IUserService;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 
 /**
  * <p>
  *  服务实现类
  * </p>
  *
  * @author java大师
  * @since 2023-03-17
  */
 @Service
 public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService {
 
     @Autowired
     private UserMapper userMapper;
 
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         User loginUser = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
         if (loginUser == null){
             throw new UsernameNotFoundException("用户名或密码错误");
         }
         loginUser.setRoles(userMapper.getRolesByUserId(loginUser.getId()));
         return loginUser;
     }
 }
2.3.2加载userDetailService

将我们自己的UserDetailService注入springsecurity

package com.ds.book.config;
 
 import com.ds.book.filter.JwtTokenFilter;
 import com.ds.book.service.impl.UserServiceImpl;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 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.builders.WebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 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.access.intercept.FilterSecurityInterceptor;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 
 @Configuration
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
 
     @Autowired
     private UserServiceImpl userService;
 
     @Bean
     public PasswordEncoder passwordEncoder(){
         return new BCryptPasswordEncoder();
     }
 
     //注入我们自己的UserDetailService
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
     }
 }

问题:前后端分离项目,通常不会使用springsecurity自带的登录界面,登录界面由前端完成,后台只需要提供响应的服务即可,且目前主流不会采用session去存取用户,后端会返回响应的token,前端访问的时候,会在headers里面带入token.

2.4JwtToken

2.4.1 JWT描述

Jwt token由Header、Payload、Signature三部分组成,这三部分之间以小数点”.”连接,JWT token长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU

token解析后长这样: header部分,有令牌的类型(JWT)和签名算法名称(HS256): { "alg": "HS256", "typ": "JWT" } Payload部分,有效负载,这部分可以放任何你想放的数据:

{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }

Signature签名部分,由于这部分是使用header和payload部分计算的,所以还可以以此来验证payload部分有没有被篡改:

HMACSHA256(

base64UrlEncode(header) + "." +

base64UrlEncode(payload),

123456 //这里是密钥,只要够复杂,一般不会被破解

)

2.4.2 pom.xml
<dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.0</version>
 </dependency>
2.4.3 JwtToken工具类
package com.ds.book.tool;
 
 
 import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.JwtBuilder;
 import io.jsonwebtoken.Jwts;
 import io.jsonwebtoken.SignatureAlgorithm;
 
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;
 import java.util.Base64;
 import java.util.Date;
 import java.util.UUID;
 
 /**
  * JWT工具类
  */
 public class JwtUtil {
 
     //有效期为
     public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
     //设置秘钥明文
     public static final String JWT_KEY = "dashii";
 
     public static String getUUID(){
         String token = UUID.randomUUID().toString().replaceAll("-", "");
         return token;
     }
 
     /**
      * 生成jtw
      * @param subject token中要存放的数据(json格式)
      * @return
      */
     public static String createJWT(String subject) {
         JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
         return builder.compact();
     }
 
     /**
      * 生成jtw
      * @param subject token中要存放的数据(json格式)
      * @param ttlMillis token超时时间
      * @return
      */
     public static String createJWT(String subject, Long ttlMillis) {
         JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
         return builder.compact();
     }
 
     private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
         SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
         SecretKey secretKey = generalKey();
         long nowMillis = System.currentTimeMillis();
         Date now = new Date(nowMillis);
         if(ttlMillis==null){
             ttlMillis= JwtUtil.JWT_TTL;
         }
         long expMillis = nowMillis + ttlMillis;
         Date expDate = new Date(expMillis);
         return Jwts.builder()
                 .setId(uuid)              //唯一的ID
                 .setSubject(subject)   // 主题  可以是JSON数据
                 .setIssuer("dashi")     // 签发者
                 .setIssuedAt(now)      // 签发时间
                 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                 .setExpiration(expDate);
     }
 
     /**
      * 创建token
      * @param id
      * @param subject
      * @param ttlMillis
      * @return
      */
     public static String createJWT(String id, String subject, Long ttlMillis) {
         JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
         return builder.compact();
     }
 
     public static void main(String[] args) throws Exception {
         String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
         Claims claims = parseJWT(token);
         System.out.println(claims);
     }
 
     /**
      * 生成加密后的秘钥 secretKey
      * @return
      */
     public static SecretKey generalKey() {
         byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
         SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
         return key;
     }
 
     /**
      * 解析
      *
      * @param jwt
      * @return
      * @throws Exception
      */
     public static Claims parseJWT(String jwt) throws Exception {
         SecretKey secretKey = generalKey();
         return Jwts.parser()
                 .setSigningKey(secretKey)
                 .parseClaimsJws(jwt)
                 .getBody();
     }
 }
2.4.4 JwtTokenFilter
package com.ds.book.filter;
 
 import com.ds.book.entity.User;
 import com.ds.book.mapper.UserMapper;
 import com.ds.book.service.IMenuService;
 import com.ds.book.service.IUserService;
 import com.ds.book.tool.JwtUtil;
 import io.jsonwebtoken.Claims;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Component;
 import org.springframework.util.StringUtils;
 import org.springframework.web.filter.OncePerRequestFilter;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 
 @Component
 public class JwtTokenFilter extends OncePerRequestFilter {
 
     @Autowired
     private IUserService userService;
     @Autowired
     private UserMapper userMapper;
 
     @Override
     protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
         //1、获取token
         String token = httpServletRequest.getHeader("token");
         if (StringUtils.isEmpty(token)){
             filterChain.doFilter(httpServletRequest,httpServletResponse);
             return;
         }
         String userId;
         try {
             Claims claims = JwtUtil.parseJWT(token);
             userId = claims.getSubject();
         } catch (Exception exception) {
             exception.printStackTrace();
             throw new RuntimeException("token非法");
         }
         User user = userService.getUserById(Integer.parseInt(userId));
         user.setRoles(userMapper.getRolesByUserId(Integer.parseInt(userId)));
         UsernamePasswordAuthenticationToken authenticationToken =
                 new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
         SecurityContextHolder.getContext().setAuthentication(authenticationToken);
         filterChain.doFilter(httpServletRequest,httpServletResponse);
     }
 }
 

在springsecurity中,第一个经过的过滤器是UsernamePasswordAuthenticationFilter,所以前后端分离的项目,我们自己定义的过滤器要放在这个过滤器前面,具体配置如下

@Override
     protected void configure(HttpSecurity http) throws Exception {
         http.csrf().disable()
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                 .and()
                 .authorizeRequests()
                 .antMatchers("/login").permitAll()
                 .anyRequest().authenticated();
         http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
         http.cors();
     }
2.4.5授权
2.4.5.1 开启preAuthorize进行收取(Controller路径匹配)

1)主启动类上添加EnableGlobalMethodSecurity注解

@EnableGlobalMethodSecurity(prePostEnabled = true)
 @SpringBootApplication
 @MapperScan("com.ds.book.mapper")
 public class BookSysApplication {
     public static void main(String[] args) {
         SpringApplication.run(BookSysApplication.class,args);
     }
 }

2)Controller方法上添加@PreAuthorize注解

@RestController
 public class HelloController {
 
     @GetMapping("/hello")
     @PreAuthorize("hasRole('ROLE_ADMIN')")
     public String hello(){
         return "hello";
     }
 }
2.4.5.2 增强方式授权(数据库表配置)

1)创建我们自己的FilterInvocationSecurityMetadataSource,实现getAttributes方法,获取请求url所需要的角色


 @Component
 public class MySecurtiMetaDataSource implements FilterInvocationSecurityMetadataSource {
 
     @Autowired
     private IMenuService menuService;
     AntPathMatcher antPathMatcher = new AntPathMatcher();
 
     //获取访问url需要的角色,例如:/sys/user需要ROLE_ADMIN角色,访问sys/user时获取到必须要有ROLE_ADMIN角色。返回     Collection<ConfigAttribute>
     @Override
     public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
         String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
         //获取所有的菜单及角色
         List<Menu> menus = menuService.getMenus();
         for (Menu menu : menus) {
             if (antPathMatcher.match(menu.getUrl(),requestURI)){
                 String[] roles = menu.getRoles().stream().map(role -> role.getRoleCode()).toArray(String[]::new);
                 return SecurityConfig.createList(roles);
             }
         }
         return null;
     }
 
     @Override
     public Collection<ConfigAttribute> getAllConfigAttributes() {
         return null;
     }
 
     @Override
     public boolean supports(Class<?> clazz) {
         return false;
     }
 }

2)创建我们自己的决策管理器AccessDecisionManager,实现decide方法,判断步骤1)中获取到的角色和我们目前登录的角色是否相同,相同则允许访问,不相同则不允许访问,

@Component
 public class MyAccessDecisionManager implements AccessDecisionManager {
     
     //1、认证通过后,会往authentication中填充用户信息
     //2、拿authentication中的权限与上一步获取到的角色信息进行比对,比对成功后,允许访问
     @Override
     public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
         Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
         for (ConfigAttribute configAttribute : configAttributes) {
             for (GrantedAuthority authority : authorities) {
                 if (authority.getAuthority().equals(configAttribute.getAttribute())){
                     return;
                 }
             }
         }
         throw new AccessDeniedException("权限不足,请联系管理员");
     }
 
     @Override
     public boolean supports(ConfigAttribute attribute) {
         return false;
     }
 
     @Override
     public boolean supports(Class<?> clazz) {
         return false;
     }
 }

3)在SecurityConfig中,添加后置处理器(增强器),让springsecurity使用我们自己的datametasource和decisionMananger

@Configuration
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
     @Autowired
     private MySecurtiMetaDataSource mySecurtiMetaDataSource;
     @Autowired
     private MyAccessDecisionManager myAccessDecisionManager;
     @Autowired
     private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
     @Autowired
     private MyAccessDeniedHandler myAccessDeniedHandler;
 
     @Autowired
     private UserServiceImpl userService;
 
     @Autowired
     private JwtTokenFilter jwtTokenFilter;
 
     @Bean
     public PasswordEncoder passwordEncoder(){
         return new BCryptPasswordEncoder();
     }
 
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
     }
 
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.csrf().disable()
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                 .and()
                 .authorizeRequests()
                 .antMatchers("/login").permitAll()
                 .anyRequest().authenticated()
                 //后置处理器,使用我们自己的FilterSecurityInterceptor拦截器配置
                 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor> () {
                     @Override
                     public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                         o.setSecurityMetadataSource(mySecurtiMetaDataSource);
                         o.setAccessDecisionManager(myAccessDecisionManager);
                         return o;
                     }
                 })
                 .and()
                 .headers().cacheControl();
         http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
         http.cors();
     }
 }
2.4.6异常处理

1)前端渲染工具类

public class WebUtils
 {
     /**
      * 将字符串渲染到客户端
      *
      * @param response 渲染对象
      * @param string 待渲染的字符串
      * @return null
      */
     public static String renderString(HttpServletResponse response, String string) {
         try
         {
             response.setStatus(200);
             response.setContentType("application/json");
             response.setCharacterEncoding("utf-8");
             response.getWriter().print(string);
         }
         catch (IOException e)
         {
             e.printStackTrace();
         }
         return null;
     }
 }

2)未登录异常处理,实现commence方法

@Component
 public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
     @Override
     public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
         Result result = new Result(401,"未登录,请先登录",null);
         String json = JSON.toJSONString(result);
         WebUtils.renderString(httpServletResponse,json);
 
     }
 }

springsecurity-jwt整合_spring_04

3)授权失败异常处理,实现Handle方法

@Component
 public class MyAccessDeniedHandler implements AccessDeniedHandler {
     @Override
     public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
         Result result = new Result(403,"权限不足请联系管理员",null);
         String s = JSON.toJSONString(result);
         WebUtils.renderString(httpServletResponse,s);
     }
 }

springsecurity-jwt整合_ide_05

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  3I1N9ysrcSyk   2023年12月08日   31   0   0 javahapi数据交换
  DF5J4hb0hcmT   2023年12月07日   50   0   0 javaArthas
MV69bd6votD4