spring security Jwt令牌 (前后端分离)

详细流程和想了解源码
请跳转,大神写的很详细
(https://blog.csdn.net/yuanlaijike/article/details/80249235)

需要的依赖包


		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		//spring security 核心包
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        //热部署
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		// jwt 生成Token令牌
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.0</version>
		</dependency>
		//小工具
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		//mybatis-plus 依赖
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.3.2</version>
		</dependency>
		// mybatis-plus 代码生成插件
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-generator</artifactId>
			<version>3.3.2</version>
		</dependency>
		<dependency>
			<groupId>org.apache.velocity</groupId>
			<artifactId>velocity-tools</artifactId>
			<version>2.0</version>
		</dependency>
		// 阿里云短信服务
		<dependency>
			<groupId>com.aliyun</groupId>
			<artifactId>aliyun-java-sdk-core</artifactId>
			<version>4.5.1</version>
		</dependency>
		
		<!--swagger-->
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>2.7.0</version>
		</dependency>
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>2.7.0</version>
		</dependency>
		//mysql驱动包
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		// redis 依赖
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
		</dependency>
		// JSON转换
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.1.37</version>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		

		<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>

		//mybatis-plus 如果自己手写xml 一定要配置
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<includes>
					<include>**/*.xml</include>
				</includes>
				<filtering>false</filtering>
			</resource>
		</resources>
	</build>

数据库 就建简单的两张表测试就行了
用户表
在这里插入图片描述
权限表
在这里插入图片描述

各种工具类

统一返回的状态码

	public interface RespCode {
    Integer OK =20000;
    Integer ERROR =20001;
}

正常请求统一返回数据格式

@Data
public class R  {
    private Integer code;
    private boolean status;
    private String message;
    private Map<String, Object> data = new HashMap<>();

    private R(){};
    public static R ok() {
        R r = new R();
        r.code = RespCode.OK;
        r.status = true;
        r.message = "success";
        return r;
    }

    public static R error() {
        R r = new R();
        r.code = RespCode.ERROR;
        r.status = false;
        r.message = "error";
        return r;
    }

    public R message(String message) {
        this.message = message;
        return this;
    }

    public R code(Integer code) {
        this.code = code;
        return this;
    }

    public R data(String key, Object value) {
        this.data.put(key, value);
        return this;
    }

    public R data(Map<String, Object> data) {
        this.data = data;
        return this;
    }
}

Security 各种处理器的返回格式

public class ResponeUtils {
    public static void out(HttpServletResponse response,R r){
    	//传入自定义工具类R
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        //统一返回的JSON数据
        response.setContentType("application/json; charset=UTF-8");
        PrintWriter writer = null;
        try {
            writer=response.getWriter();
            //通过Response输出流 返回前端
            writer.write(mapper.writeValueAsString(r));
        } catch (IOException e) {
            throw  new CustomException(20001,r.getMessage());
        }finally {
            writer.close();
        }
    }
}

Jwt

public class JwtUtils {
	//过期时间
    private static final long EXPIREDTIME=60*60*24*1000;
    // 密钥 ()
    private static final String salt="chao";
    public static String createToken(String username){
  		//传入username 成功token
        String token = Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIREDTIME))
                .signWith(SignatureAlgorithm.HS512, salt)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
        return token;
    }
    	//通过Token 解析出 username
    public static String getUsernameToken(String token){
        String username = Jwts.parser().setSigningKey(salt).parseClaimsJws(token).getBody().getSubject();
        return username;
    }
}

密码工具类 (直接写在配置类中也可以)

@Configuration
public class PasswordEncoderConfig  {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return  new BCryptPasswordEncoder();
    }
}

异常工具类

自定义异常

	@Data
@AllArgsConstructor
@NoArgsConstructor
public class CustomException extends RuntimeException {
    private Integer code;
    private String message;
}

全局异常处理类

@RestController
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public R GlobalException(Exception e){
        return R.error().message("执行了全局异常");
    }

    @ExceptionHandler(CustomException.class)
    public R CustomException(CustomException e){
        return R.error().code(e.getCode()).message(e.getMessage());
    }
}

重写UserDetailsService方法 作用是 通过传入的username 从数据库中返回用户信息 给 UserDetails 设置值

	@Service
	public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    	//根据用户名从数据库中返回 user
        User user = userService.gerUserByUsername(username);
        //如果没有该用户 直接抛异常 交给failureHandler处理
        if(user==null){
            throw new InternalAuthenticationServiceException("用户不存在");
        }
        CumtomUserDetails userDetails=new CumtomUserDetails();
        //通过用户的id 查找权限(就是数据中的路径)
        List<String> permissons = userService.getPermissonByUserId(user.getId());
        // 设值 然后返回
        userDetails.setUsername(user.getUsername());
        userDetails.setPasswrod(user.getPassword());
        userDetails.setPermissons(permissons);
        return userDetails;
    }
}

重写 UserDetails

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CumtomUserDetails implements UserDetails {
    private String username;  //用户名
    private String passwrod;  //密码
    private List<String> permissons; //权限 实际就是 路径的集合 ["/teacher","/student"]
    private Boolean isAccountNonExpired; // 账号是否过期
    private Boolean isAccountNonLocked; //账号是否锁定
    private Boolean isCredentialsNonExpired; //密码是否过期
    private Boolean isEnabled; // 是否被禁用

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { 
    	//遍历 private List<String> permissons; 将路径字符串转换为 GrantedAuthority 对象
        Collection<GrantedAuthority> authorities =new ArrayList<>();
        for (String permisson : permissons) {
            if(permisson==null) continue;
            GrantedAuthority authority=new SimpleGrantedAuthority(permisson);
            authorities.add(authority);
        }

        return authorities;
    }

    @Override
    public String getPassword() {
        return this.passwrod;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

	//简单的统一返回true 实际可以通过UserDetailsService 从数据库中返回的
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

核心配置类

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder; //密码加密
    @Resource
    private UserDetailsService userDetailsService; //  //重写 userDetailsService  通过用户名去数据库加载密码
    @Autowired
    private CustomSuccessHandler customSuccessHandler; // 认证成功   处理 返回JSON数据
    @Autowired
    private CustomFailureHandler customFailureHandler; // 认证失败  处理 返回JSON数据
    @Autowired
    private CumtomAuthorizeFilter cumtomAuthorizeFilter; // 重写 OncePerRequestFilter 通过校验请求头中是否携带有令牌
    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler; // 登陆了 没有权限 是触发
    @Autowired
    private CustomAuthorizationEntryPoint authorizationEntryPoint;// 未登陆 触发
    @Autowired
    private CustomLayoutHandler customLayoutHandler; // 退出时 处理
    @Autowired
    private SmsSecurityConfig smsSecurityConfig; //短信登陆的配置类
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.apply(smsSecurityConfig)  //短信登陆配置类 在下个博客补上
                .and()
            //必须放在UsernamePasswordAuthenticationFilter之前,如果请求头中替代有Token 就直接放行,没有的话 再进行用户名密码登陆
                .addFilterBefore(cumtomAuthorizeFilter, UsernamePasswordAuthenticationFilter.class)
                //异常处理器
                .exceptionHandling()
                .authenticationEntryPoint(authorizationEntryPoint)
                .accessDeniedHandler(customAccessDeniedHandler)
                .and()
// 表单登陆 <input name="username">  <input name="password"> 如果有需求可以修改 通过 passwordParameter usernameParameter修改
                .formLogin()
                // 登陆的接口 自己定义
                .loginProcessingUrl("/login")
                // 成功处理器
                .successHandler(customSuccessHandler)
                // 失败处理器
                .failureHandler(customFailureHandler)
                .and()
           		// 授权请求
                .authorizeRequests()
                //permitAll 不需要任何授权 就可以访问
                .antMatchers("/login","/logout","/sms/login","/sms/*").permitAll()
                //authenticated 需要登陆了 才能进行访问 
                .antMatchers("/getUserInfo/*","/getMenu/*").authenticated()
                //自定义权限控制器  其他所有请求 都要进行校验 返回True表示通过 返回false表示不能访问
                .anyRequest().access("@rbacService.hasPermission(request,authentication)")
                .and()
                //退出处理  自定义url  处理器
                .logout().logoutUrl("/logout")
                .addLogoutHandler(customLayoutHandler)
                .and()
                //修改session策略 无状态应用 STATELESS 因为没用到session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //解决跨域访问
                .cors().and().csrf().disable();

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	//配置 AuthenticationManager 放入自定义 userDetailsService  加密器
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }

    @Override
    	//放行静态资源 前后端分离了 不配置也没关系 如果使用swagger的话 可以配置一下
    public void configure(WebSecurity web) throws Exception {
       web.ignoring().antMatchers("/api/**",
                "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"
               );
    }

	//处理跨域问题
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowCredentials(true); 
        configuration.setAllowedOrigins(Arrays.asList("*")); //允许所有的跨域访问 有需要自己重写
        configuration.setAllowedMethods(Arrays.asList("*")); //允许所有方法的请求 有需要自己重写
        configuration.setAllowedHeaders(Arrays.asList("*")); //请求头  有需要自己重写
        configuration.setMaxAge(Duration.ofHours(1));
        source.registerCorsConfiguration("/**",configuration); //代表可以访问所有的资源
        return source;
    }

重写 OncePerRequestFilter 用来查看请求头中是否携带有Token令牌 有就生成 anthentication(后面就不需要再验证了) 如果没有的话就放行 进入 UsernamePasswordAuthenticationFilter 进行用户名密码 校验

	@Component
public class CumtomAuthorizeFilter extends OncePerRequestFilter {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {

		//通过 getAuthentication 看是否生成 authentication 如果有的话 就放入context中 后面就不需要验证了
		//返回null 就放行 进入 UsernamePasswordAuthenticationFilter 进行用户名密码 校验
		//第一次登陆 肯定没有token 直接走 UsernamePasswordAuthenticationFilter
			
        UsernamePasswordAuthenticationToken authentication=getAuthentication(request);
        if(authentication!=null){
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
    	//查看请求头是否有 token
        String token = request.getHeader("token");
        if(!StringUtils.isEmpty(token)){
        	//通过jwt工具类 返回用户名
            String username = JwtUtils.getUsernameToken(token);
            //从redis中取出 路径字符串 转换为 权限列表
            List<String> permissons = (List<String>) redisTemplate.opsForValue().get(username);
            Collection<GrantedAuthority> authorities=new ArrayList<>();
            for (String permisson : permissons) {
                if(StringUtils.isEmpty(permisson)){
                    continue;
                }
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permisson);
                authorities.add(authority);
            }
            if(!StringUtils.isEmpty(username)){
            	//前面都成立的话  加载用户的详细信息 生成 UsernamePasswordAuthenticationToken 令牌
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
            }
            return null;
        }
        return null;

    }
}

认证成功处理器

第一个功能 将用户名 和 用户权限(路径) 放入redis中 上面的filter 就可以通过用户名去redis中取出来了
第二个功能 通过response对象 将token返回 回前台 再请求的时候 前台回在请求头中携带这个token 在上面的filter 中直接生成 authentication (认证主体)

	@Component
public class CustomSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        CumtomUserDetails userDetails = (CumtomUserDetails) authentication.getPrincipal();
        String token = JwtUtils.createToken(userDetails.getUsername());
        redisTemplate.opsForValue().set(userDetails.getUsername(),userDetails.getPermissons());
        ResponeUtils.out(response, R.ok().message("登陆成功").data("token",token));

    }
}

认证失败处理器

拦截各种类型的异常 不细分的话 直接 ResponeUtils.out(response, R.error());

	@Component
public class CustomFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    	 if(exception instanceof InternalAuthenticationServiceException){
            ResponeUtils.out(response, R.error().message("用户不存在"));
         }
        if(exception instanceof BadCredentialsException){
            ResponeUtils.out(response, R.error().message(exception.getMessage()));
        }
        if(exception instanceof UsernameNotFoundException){
            ResponeUtils.out(response, R.error().message("用户不存在"));
        }
        if(exception instanceof DisabledException){
            ResponeUtils.out(response, R.error().message("用户被禁用"));
        }
        if(exception instanceof LockedException){
            ResponeUtils.out(response, R.error().message("账户锁定"));
        }
        if(exception instanceof AccountExpiredException){
            ResponeUtils.out(response, R.error().message("账户过期"));
        }
        if(exception instanceof CredentialsExpiredException){
            ResponeUtils.out(response, R.error().message("证书过期"));
        }
        else {
            ResponeUtils.out(response, R.error().message("其他错误"));
        }

    }
}

成功退出 处理器
主要是将redis中的数据给清理掉 然后返回JSON数据就行了

@Component
public class CustomLayoutHandler implements LogoutHandler {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String token = request.getHeader("token");
        System.out.println(token);
        if(token!=null){
            String username = JwtUtils.getUsernameToken(token);
            System.out.println(username);
            redisTemplate.delete(username);
        }
        try {
            new ObjectMapper().writeValue(response.getOutputStream(), R.ok().message("退出成功"));
        } catch (IOException e) {
            ResponeUtils.out(response,R.error());
        }
    }
}

未登陆处理器 AuthenticationEntryPoint

@Component
public class CustomAuthorizationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponeUtils.out(response,R.error().message("请登陆再访问"));
    }
}

登陆了 但是权限不够 AccessDeniedHandler

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponeUtils.out(response, R.error().message("无权访问"));
    }
}

自定义权限控制器
通过 getPrincipal() 从authentication 取出主体 其实就是前面OncePerRequestFilter 中存入UsernamePasswordAuthenticationToken中的
UserDetails, 然后取出里面的路径字符串 通过遍历和请求的url比较 里面有的话 就返回true表示有权限 返回false就表示没有权限

@Component("rbacService")
public class MyRbacService {

    public boolean hasPermission(HttpServletRequest request, Authentication authentication){
    	
        Object principal = authentication.getPrincipal();
        /*if(request.getRequestURI().indexOf("admin")==-1){
            return true;
        }*/
        if(principal instanceof UserDetails){
            CumtomUserDetails userDetails=(CumtomUserDetails) principal;
            boolean flag=false;
            List<String> permissons = userDetails.getPermissons();
            for (String permisson : permissons) {
                if(request.getRequestURI().equals(permisson)){
                    flag= true;
                    break;
                }
            }
            return flag;
        }
        return false;
    }
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值