基于springBoot+springSecurity+jwt实现前后端分离用户权限认证


1. 项目说明

  主要基于前后端分离情况下用户权限认证, 当用户登录认证成功后,每个用户会获取到自己的token,在请求其他接口时只需携带token即可,后端会通过token来识别用户身份。springSecurity也有很多种权限认证方式,本项目主要实现基于接口授权,也就是说通过注解给controller赋予权限,用户只有拥有某个接口的权限才能成功访问这个接口,从而实现不同用户拥有不同访问权限;


2. 项目结构

1、 使用maven引入依赖在pom.xml中添加以下依赖(springSecurity+jwt)

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

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

2、通过 application.properties配置项目系统库和jwt认证

spring.datasource.mg.cboard.url=jdbc:mysql://192.168.0.65:3306/pharos?useUnicode=true&characterEncoding=UTF-8
spring.datasource.mg.cboard.username=pharos
spring.datasource.mg.cboard.password=Ketech@123456
spring.datasource.mg.cboard.driver-class-name=com.mysql.cj.jdbc.Driver

jwt.tokenHeader: Authorization
#JWT存储的请求头
jwt.secret: ketech-pharos
#JWT加解密使用的密钥
jwt.expiration: 604800 
#JWT的超期限时间(60*60*24*7)
jwt.tokenHead: 'Bearer '
#JWT负载中拿到开头

3、 用户权限关系表导入数据库

** 表结构在项目源码中可自行下载

Java权限申请 java权限认证_Java权限申请


4、 编写jwt工具类 主要功能是:用户信息加密,除了登录认证时账号密码需要明文传输,其他地方都需要加密后传输,通过jwt对用户加密后形成token,用来进行用户认证。添加token时效,每个用户请求接口不是永久的,如果是永久的那么他就没有存在的意义了;token验证,token有自己加密协议, 来保证接口安全;token解密,将token翻译成明文信息;

@Component
public class JwtUtils {
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private  Long expiration;


    /**
     * 创建token
     * @param username 用户名
     * @return
     */
    public  String generateToken(String username) {
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, secret)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();

    }


    /**
     * 从token中获取用户名
     * @param token
     * @return
     */
    public  String getUserNameFromToken(String token){
        return getTokenBody(token).getSubject();
    }



    /**
     *  是否已过期
     * @param token
     * @return
     */
    public  boolean isExpiration(String token){
        return getTokenBody(token).getExpiration().before(new Date());
    }

    private  Claims getTokenBody(String token){
        Claims claims=null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            throw new DescribeException(ExceptionEnum.valid_token);
        }
        return claims;
    }

}

4、 配置springSecurity 1 简介 他的核心功能是: 1认证:这个地方需要整合jwt对用户进行认证 2 授权:认证完成后,在数据库中获取用户的权限。接口会有自己的权限,只有当用户权限和接口所需权能匹配时,用户才能成功访问接口; 3 过滤器:其核心是一条过滤链,只有当每个filter通过了才能请求到接口,这个中间过程需要自己配置,并不是所有接口都需要权限,比如登录接口,异常接口 ;

Java权限申请 java权限认证_jwt_02


2 核心代码简介
1> 用户拦截

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsServiceImpl userDetailsServices;

//    @Autowired
//    FavUserDetailService favUserDetailService;
    /**
     * 验证码拦截器
     */
//    @Autowired
//    private VerifyCodeFilter verifyCodeFilter;

    /**
     * 登录成功逻辑
     */
    @Autowired
    AuthenticationSuccessHandlerImp authenticationSuccessHandler;
    /**
     * 登录失败逻辑
     */
    @Autowired
    AuthenticationFailureHandlerImp authenticationFailureHandler;

//    /**
//     * 无权限拦截器
//     */
//    @Autowired
//    RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    /**
     * 无权访问 JSON 格式的数据
     */
    @Autowired
    private AccessDeniedHandlerImp accessDeniedHandler;

    @Autowired
    private AuthenticationEntryPointImp authenticationEntryPointImp;


    @Override
    public void configure(WebSecurity web) throws Exception {
        //放行静态资源
        web.ignoring().antMatchers("/v2/api-docs",
                "/swagger-resources/configuration/ui",
                "/swagger-resources",
                "/swagger-resources/configuration/security",
                "/swagger-ui.html")
                .antMatchers(HttpMethod.POST, "/user/login","/swagger-ui.html");
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        
//        http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域访问
        http.cors();
       //放行swagger
        http.authorizeRequests()
                .antMatchers("/v2/api-docs",//swagger api json
                        "/swagger-resources/configuration/ui",//用来获取支持的动作
                        "/swagger-resources",//用来获取api-docs的URI
                        "/swagger-resources/configuration/security",//安全选项
                        "/swagger-ui.html",
                        "/webjars/**").permitAll();
        //关闭csrf
        http.csrf().disable()
                .sessionManagement()// 基于token,所以不需要session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //未登陆时返回 JSON 格式的数据给前端
                .httpBasic().authenticationEntryPoint(authenticationEntryPointImp)
                .and()
                .authorizeRequests()
                //任何人都能访问这个请求
                .antMatchers("/swagger-ui.html","/user/login","/user/get").permitAll()
                //除上面配置的路径外,所有的请求都需要进行认证后才能访问
                .anyRequest().authenticated()
                .and()
                .formLogin()
                // (登录页面或是提示用户未登录的页面)
                .loginPage("/user/get").permitAll()
                //认证通过后的事件处理(在这里返回token)
                .loginProcessingUrl("/user/login")
                // 登录成功
                .successHandler(authenticationSuccessHandler)
                // 登录失败
                .failureHandler(authenticationFailureHandler)
                .permitAll()
                .and()
                //设置登录注销url,这个无需我们开发,springsecurity已帮我们做好
                .logout().logoutUrl("/user/logout").permitAll()
                .and()
                //配置 记住我 功能,
                .rememberMe().rememberMeParameter("rememberme");
//                 防止iframe 造成跨域
//                .and()
//                .headers()
//                .frameOptions()
//                .disable();
                   http.httpBasic().disable();
                  //.and().httpBasic(); //开启HTTP Basic后 ,可用postman通过携带用户名+密码直接请求接口

        // 禁用缓存
        http.headers().cacheControl();

        // 添加自定义 JWT 过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);

        // 无权访问 JSON 格式的数据
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);

        //没有认证时,在这里处理结果,不要重定向
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPointImp);

    }

    @Bean
    public JwtAuthenticationTokenFilters jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilters();
    }
    //    //密码编码器
//    @Bean
//    public PasswordEncoder passwordEncoder(){
//        return NoOpPasswordEncoder.getInstance();
//    }

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

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServices).passwordEncoder(passwordEncoder());
    }


}

1 @EnableWebSecurity
利用@EnableWebSecurity 注解继承 WebSecurityConfigurerAdapter的类,这样就构成了 Spring Security 的配置;

2 @EnableGlobalMethodSecurity(prePostEnabled = true)
配置基于接口授权

3 WebSecurityConfigurerAdapter
通过继承次类重写他的方法来配置权限拦截;

Java权限申请 java权限认证_java_03

HttpSecurity常用方法和说明:

Java权限申请 java权限认证_spring_04


Java权限申请 java权限认证_spring_05


2> 用户授权 前面我们通过jwt对用户进行了认证,WebSecurityConfigurerAdapter对通过认证的用户进行权限过滤,接下来需要实现UserDetailsService的loadUserByUsername方法对用户进行授权;

@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    UserService userService;

    /**
     * 根据用户名获取用户 - 用户的角色、权限等信息
     */
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        UserDetails userDetails = null;
        try {
            PharosUser user = userService.getUserByName(userName);
            List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
            List<MenuIndexDto> list = userService.listByUserId(user.getUserId());
            for (MenuIndexDto dto : list) {
                String authority = dto.getPermission();
                if (!("").equals(authority) & authority != null) {
                    authList.add(new SimpleGrantedAuthority(dto.getPermission()));
                }
            }
            userDetails = new User(user.getUserName(), user.getPassword(), true, true, true, true, authList);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return userDetails;
    }

}

5 postma测试 1 获取通过用户名+密码获取token

Java权限申请 java权限认证_jwt_06

2 访问需要权限的接口

Java权限申请 java权限认证_java_07


Java权限申请 java权限认证_jwt_08

3 访问用户没有访问权限的接口

Java权限申请 java权限认证_spring_09


Java权限申请 java权限认证_java_10