Spring security+jwt+前后端分离的登录



本文以springboot+spring securit +jwt 实现前后端分离的场景,登录之后返回token完成之后的鉴权

1.入门知识

spring-security
├── 核心 - spring-security-core.jar
├── Remoting - spring-security-remoting.jar
├── Web - spring-security-web.jar
├── 配置 - spring-security-config.jar
├── LDAP - spring-security-ldap.jar
├── OAuth 2.0核心 - spring-security-oauth2-core.jar
├── OAuth 2.0客户端 - spring-security-oauth2-client.jar
├── OAuth 2.0 JOSE - spring-security-oauth2-jose.jar
├── ACL - spring-security-acl.jar
├── CAS - spring-security-cas.jar
├── OpenID - spring-security-openid.jar
└── 测试 - spring-security-test.jar


[

](https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/)

2.配置

1.核心配置类

@EnableWebSecurity 开启Spring Security的功能
需要继承WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity // 开启Spring Security的功能

//prePostEnabled = true表明开启 @PreAuthorize(方法之前验证授权),@PostAuthorize(授权方法之后被执行),@PostFilter(在方法执行之后执行,用于过滤集合),@PreFilter(在方法执行之前执行,用于过滤集合);功能强大,所以其使用需要借助spring的EL表达式;
//securedEnabled = true,表明使用 @Secured 注解 开启权限
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter  {

    @Autowired
    @Qualifier("myUserDetailService")
    private UserDetailsService userDetailsService;
    @Autowired
    private UnauthorizedEntryPoint unauthorizedEntryPoint;
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    //放行名单
    private final static String[] WHITE_URL_LIST = {
            "/swagger-ui.html/**",
            "/api/**",
            "/resources/**",
            "/auth/**",
            "/doc.html*",
            "/v2/api-docs",
            "/swagger-resources/**",
            "/static/**",
            "/**/*.js",
            "/**/*.html",
            "/**/*.css",
            "/*.txt",
            "/login/**",
            "/auth/**",

    };


    /**
     * 核心配置
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("我进来了");

        http.csrf().disable().cors()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //session创建策略,永不
                .and()
                .httpBasic()
                // 未经过认证的用户访问受保护的资源
                .authenticationEntryPoint(unauthorizedEntryPoint)
                .and()
                .authorizeRequests()
                .antMatchers(WHITE_URL_LIST).permitAll() //auth服务放行名单
                .anyRequest().authenticated()
                .and()
                .formLogin().disable()
                .logout().logoutSuccessHandler((httpServletRequest, httpServletResponse, authentication) ->
                ResponseUtils.responseReturn(httpServletResponse, Result.ok("注销成功")))
                .permitAll();
        //异常处理
        http.exceptionHandling()
                // 已经认证的用户访问自己没有权限的资源处理
                .accessDeniedHandler((httpServletRequest, httpServletResponse, e) ->
                        ResponseUtils.responseReturn(httpServletResponse, Result.error(ResultEnum.FORBIDDEN.getCode(),ResultEnum.FORBIDDEN.getMsg())))
                .and().addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry
                = http.authorizeRequests();

    }


    /**
     * 认证管理器配置
     */

    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 密码处理
     *
     * @param auth
     * @throws Exception
     */

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }


    /**
     * 装载BCrypt密码编码器,官方推荐的,
     *  是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10.
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


}

其中有三个组件

a.UserDetailsService

通过实现该接口完成自定义的身份认证,若不配置则有 spring security 自定义生成;

/**
 * Security身份认证之UserDetailsService
 *
 */

@Service("myUserDetailService")
public class MyUserDetailService implements UserDetailsService {

    @Resource
    private UserService userService;


    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {


        String regPattern =  "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$";
        Pattern pattern = Pattern.compile(regPattern);
        Matcher matcher = pattern.matcher(userName);
        boolean isMatch = matcher.matches();

        //若是手机号
        if (isMatch){

            final com.fufu.idea.entity.user.User user = userService.selectByPhone(userName);

            //TODO 后期需要查询出权限列表?或者其他放是实现后台权限
            if (ObjectUtils.isEmpty(user)){
                throw new UsernameNotFoundException(String.format("账号'%s'不存在", userName));
            }
            // TODO 查该用户拥有的角色,来分配具体的访问权限(或者没有手机号登录的账号不返回token直接放开可以浏览的接口)
             return new User(userName,  BCrypt.hashpw(userName, BCrypt.gensalt()),new ArrayList<>());
        }else {
            throw new UsernameNotFoundException(String.format("账号'%s'不存在", userName));
        }

    }
}

该接口只有一个 loadUserByUsername() 方法,返回UserDetails类型,实现中需要具体返回org.springframework.security.core.userdetails.User类,不要和项目中的User类搞混;

构造函数中的三个参数:
username:用户名

password: 密码

authorities: 权限集合

若自定义的用户名没有查到具体的账号信息需要抛出 UsernameNotFoundException 这样可以使用通用配置后的统一异常返回;这里建议用户名查找设置为系统里用户的唯一值来进行查找,例如手机号或者登录账号;


b.AuthenticationEntryPoint

当用户请求了一个受保护的资源,但是用户认证没有通过,那么抛出异常就会被 ExceptionTranslationFilter 捕获,之后会调用 AuthenticationEntryPoint.Commence()方法;
security异常统一处理方式,实现AuthenticationEntryPoint接口

/**
 * <p>
 * security异常统一处理方式
 * </p>
 *
 * @author chenziyuan
 * 
 */
@Component
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException,
            ServletException {
        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
            if (e instanceof BadCredentialsException) {
            ResponseUtils.responseReturn(httpServletResponse, Result.error(ResultEnum.LOGIN_ERROR.getCode(), ResultEnum.LOGIN_ERROR.getMsg()));
        } else {
            ResponseUtils.responseReturn(httpServletResponse,  Result.error(ResultEnum.FORBIDDEN.getCode(), ResultEnum.FORBIDDEN.getMsg()));
        }

    }
}



c.OncePerRequestFilter

OncePerRequestFilter是Spring Boot里面的一个过滤器抽象类,其同样在Spring Security里面被广泛用到,通常用到只执行一次的请求;
重写 doFilterInternal 方法:
主要是解析token里的信息是否正确

/**
 * @Author: chenziyuan.
 * @Description: 登录验证令牌解析过滤器
 * @Date: 2020/4/9
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {


    private UserDetailsService userDetailsService;

    @Autowired
    public JwtAuthenticationTokenFilter(@Qualifier("myUserDetailService") UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authHeader = request.getHeader(SysConstant.TOKEN_HEADER);
       /* if(SpringContextUtil.getBean(Environment.class).getActiveProfiles()[0].equals(ProfileConstant.PROFILE_DEV)){
            //测试环境下免登录
            UserDetails userDetails = userDetailsService.loadUserByUsername(ProfileConstant.PROFILE_DEV_USER);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
            UserInfo userInfo = new UserInfo();
            userInfo.setAccount(ProfileConstant.PROFILE_DEV_USER);
            userInfo.setUserId(1);
            userInfo.setRoleId(1);
            userInfo.setPassword("123456");
            response.setHeader(SysConstant.TOKEN_HEADER, JwtHelper.generateToken(userInfo));

        }else{*/
        //&& authHeader.startsWith(TOKEN_PREFIX)
        if (authHeader != null && !"null".equals(authHeader)) {
            String authToken = authHeader;
            //令牌登录的用户信息
            String userName = JwtUtils.getUsernameFromToken(authToken);


            if (StringUtils.isEmpty(userName)) {
                ResponseUtils.responseReturn(response, 401, Result.error(401, "token无效"));
            }
            //获取过期时间

            if (JwtUtils.isTokenExpired(authToken)) {
                ResponseUtils.responseReturn(response, 401, Result.error(401, "token过期"));
            }
            //获取生成的自定义token信息

            //获取userDetail
            if (SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(userName);

                if (JwtUtils.validateToken(authToken)) {
                    //判断令牌是否有效
                    if (validateToken(userName,userDetails)) {
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    } else {
                        //TODO 将来若要存缓存则记得将缓存中的token删除
                        ResponseUtils.responseReturn(response, 401, Result.error(401, "token无效"));
                    }
                }
            }
        }
        filterChain.doFilter(request, response);
    }


    /**
     * 校验令牌信息
     *
     * @param userName         登录用户名
     * @param
     * @param userDetails 用户信息
     * @return boolean
     */
    private boolean validateToken(String userName, UserDetails userDetails) {


        String regPattern = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$";
        Pattern pattern = Pattern.compile(regPattern);

        Matcher matcher = pattern.matcher(userDetails.getUsername());
        boolean isMatch = matcher.matches();

        //若是手机号
        if (isMatch) {
            return userDetails.getUsername().equals(userName);
        }

        return false;

    }


}



3. 登录入口处理

登录接口要生成token,并将用户信息spring security中的上下文供以后验证用

    @ApiOperation("登录")
    @PostMapping()
    public Result login(@Validated @RequestBody UserLoginDTO dto){
        //获取spring security 上下文
        final SecurityContext context = SecurityContextHolder.getContext();
		
        //生成token
        final String token = JwtUtils.generateToken(dto.getPhone());
		
        final UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(dto.getPhone(),dto.getPhone());
        context.setAuthentication(usernamePasswordAuthenticationToken);
        return Result.ok("登录成功",token);

    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值