springsecurity整合jwt的权限管理

前言

springsecurity作为和shiro并驾齐驱的安全框架,我从工作中发现他们其实功能都是差不多的,只不过springsecurity难度更加大一点,很多接口和类都需要查文档才能梳理出来。不过springsecurity有着spring完美的加持,加之现在微服务大行其道,能在springsecurity的基础上构建基于SpringAuth2.0的分布式安全管理,所以新的项目使用springsecurity更加好。springsecurity也和他官网所说的那样非常spring-非常容易扩展。

主要类的功能概述

  • AuthenticationToken: 所有请求都会封装成AuthenticationToken,再交给AuthenticationManager去验证,核心实现就是UsernamePasswordAuthenticationToken.
  • AuthenticationManager :这个接口是所有认证管理的中心,所有的请求都会将请求信息封装为Authentication的实现类,再经过它的authenticate(Authentication authentication)方法进行认证或者授权,返回一个经过认证或者授权的Authentication对象.他有许多实现,最重要一个核心实现就是ProviderManager(图1),最后调用这个实现类的authenticate()方法(图2).这个方法的主要内容是调用AuthenticationProvider进行验证和授权.
图1:ProviderManager
图2
  • AuthenticationProvider :AuthenticationManager的authenticate方法最终调用的就是AuthenticationProviderauthenticate()的方法,当然这个也是非常的spring(为你提供了各种各样的实现),我们最重要的当然是基于数据库(图3)的验证方式,也就是DaoAuthenticationProvider,這也是默认的验证方式.
图3
  • UserDetailsService : 这个接口主要定义loadUserByUsername(String name)方法,也就是根据用户名从数据库中查询用户,所以需要用户提供自己的实现.AuthenticationProvider验证的核心原理就是:从UserDetailsService中查询数据库中用户的密码,再和用户登录的密码比较,如果匹配就说明验证成功,也就是additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)方法(图4).

    图4

  • AuthenticationSuccessHandlerAuthenticationFailureHandler:验证/认证成功和失败都是通过Handler来处理(图5、图5-1),这个比较简单.一般我们在验证成功以后生成token,认证成功以后返回成功标识即可。在AbstractAuthenticationProcessingFilter中处理失败和成功。

图5
图5-1
  • SecurityContext:当所有的验证成功以后,返回一个Authentication,这个就是用户的回话上下文,我们需要一个容器把他保存起来,这个时候就是SecurityContext来做,SecurityHolder.getSecruityContext()就可以得到用户信息.
图6

主要流程.png
流程时序图

主要流程概述

springsecurity主要有两个功能:

  • 验证:即Authenrization,主要解决"你是谁",也就是登录的时候验证你的合法性、记录用户的权限信息,验证通过后返回token.
  • 认证:即Authentication,主要解决"你能做什么",用户拿着token去请求除了登录退出之外的其他资源是否有相应的权限控制.

所以基本上本demo就是围绕这两个核心的流程展开的

新建jwt工具类,加密解密jwt,此处略

新建用户、角色、权限表,以及中间表

注意:用户表要实现UserDetails ,角色表要实现GrantedAuthority

  • 用户表
@Data
@Entity
@Table(name = "user")
@ToString
public class MyUser extends BaseEntity implements UserDetails {
    /**
     *  @JoinTable:name-中间表的名字
     *  JoinColumn:当前表的referencedColumnName的字段(id)在中间表的字段名字(user_id)
     *  inverseJoinColumns: 关联外键表的referencedColumnName的字段(id)在中间表的字段名字(role_id)
     */
    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    private List<Role> roles;

    @Column(unique = true, length = 32, columnDefinition = "varchar(32)  DEFAULT '' COMMENT '用户名'")
    private String username;

    @Column(length = 50, columnDefinition = " varchar(50) DEFAULT '' COMMENT '密码'")
    private String password;

    @Override
    public String getPassword() {
        return password;
    }

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

    /**
     * 获取权限列表
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return  roles;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • 角色表
@Data
@Entity
public class Role extends BaseEntity implements GrantedAuthority{
    @Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '角色名称/菜单名'")
    private String name;

    @Override
    public String getAuthority() {
        return name;
    }

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinTable(name = "role_permission", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "permit_id", referencedColumnName = "id"))
    private List<Permission> permissions;

    @Column(length = 1024, columnDefinition = "varchar(1024) default '' COMMENT '内容'")
    private String descpt;
    @Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '角色编号'")
    private String code;
    @Column(length = 10, columnDefinition = "int(10) COMMENT '插入者id'")
    private  Integer insertUid;

}
  • 权限表
@Data
@Entity
public class Permission  extends  BaseEntity{
    @Column(columnDefinition = "varchar(64) default '' COMMENT '权限名称'")
    private String name;

    /*@ManyToMany(mappedBy = "permissions")
    private List<Role> roles;*/

    @Column(length = 10, columnDefinition = "int(10) COMMENT '父菜单id'")
    private Integer pid;
    @Column(length = 10, columnDefinition = "int(10) COMMENT '菜单排序'")
    private Integer zindex;


    @Column(length = 1, columnDefinition = "int(1) COMMENT '权限分类(0 菜单;1 功能)'")
    private Integer istype;
    @Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '权限描述'")
    private String descpt;
    @Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '图标'")
    private String icon;
    @Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '代号'")
    private String code;
    @Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '菜单url'")
    private String page;
}

实现UserDetailsService

@Service
public class JwtUserService implements UserDetailsService {
    private  static  Logger LOGGER = LoggerFactory.getLogger(JwtUserService.class);
    @Autowired
    UserDao userDao;

    @Autowired
    private PasswordEncoder passwordEncoder;
    /**
     * 从数据库中查询用户,密码应该是数据库加密的密码,但是这里和登录的时候一致,使用写死的密码
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUser user = new UserDao().findUserByUsername(username);
        String password = passwordEncoder.encode("123456");
        user.setPassword(password);
        LOGGER.info("查询到用户信息:{}",user.toString());
        return user;
    }
    /**
     * 用户注册
     * @param user
     */
    public  void regisUser(MyUser user){

    }

    public void deleteUserJwt(){

    }

验证

新建JwtAuthenticationFilter类继承UsernamePasswordAuthenticationFilter,这个过滤器拦截"/login"路径(其实这个是默认的,写出来方便看而已),拦截后生成AuthenticationToken交给AuthenticationManager去验证,验证成功就生成jwt返回给发起者.

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {


    private  static  Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationFilter.class);


    private AuthenticationManager authenticationManager;

    /**
     * 在构造器中设置拦截的路劲,默认拦截的是"/login"
     * 在构造器中设置AuthenticationManager
     */
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager){
        super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
        this.authenticationManager=authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //从json中获取username和password
        UsernamePasswordAuthenticationToken token = null;
        try {
            String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
            String username = null, password = null;
            if (StringUtils.hasText(body)) {
                JSONObject jsonObj = JSONObject.parseObject(body);
                username = jsonObj.getString("username");
                password = jsonObj.getString("password");
            }
            if (username == null){
                username = "";
            }
            if (password == null){
                password = "";
            }
            username = username.trim();
            token = new UsernamePasswordAuthenticationToken(username,password);
            LOGGER.info("get user info from login success,name:{}",token.getName());
        } catch (IOException e) {
            LOGGER.error("get user info from login failed,reason:{}",e.getMessage());
        }
        //封装后的token最终是交给provider来处理
        Authentication authenticate = authenticationManager.authenticate(token);
        return authenticate;
    }

    /**
     * 验证成功之后的回调,可以自己实现AuthenticationSuccessHandler处理(JwtLoginSuccessHandler)
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        MyUser user= (MyUser) authResult.getPrincipal();
        String jwt = JwtTokenUtils.createToken(user);
        StringBuffer buffer = new StringBuffer(CommonConst.TOKEN_PREFIX);
        buffer.append(jwt);
        LOGGER.info("authentication success,user:【{}】,jwt:【{}】",user.toString(),buffer.toString());
        response.setHeader(CommonConst.JWTHEADER, buffer.toString());
    }

    /**
     * 验证失败之后的回调,可以自己实现AuthenticationFailureHandler处理(JwtLoginFailureHandler)
     * @param request
     * @param response
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        String message = failed.getCause().getMessage();
        LOGGER.error("authentication failed, reason:{}",message);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
    }
}

认证

新建JwtAuthenrizationFilter继承BasicAuthenticationFilter,取出用户的jwt,解密jwt

public class JwtAuthenrizationFilter extends BasicAuthenticationFilter {
    Logger LOGGER = LoggerFactory.getLogger(JwtAuthenrizationFilter.class);

    @Autowired
    JwtUserService userService;

    public JwtAuthenrizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /**
     * @param tokenHeader
     * @return
     */
    protected UsernamePasswordAuthenticationToken getToken(String tokenHeader) {
        LOGGER.info("Authenrization jwt:{}",tokenHeader);
        String token = tokenHeader.replace(CommonConst.TOKEN_PREFIX, "");
        String name = JwtTokenUtils.getUserNameByToken(token);
        LOGGER.info("Authenrization username:{}",name);
        UserDetails userDetails = userService.loadUserByUsername(name);
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        UsernamePasswordAuthenticationToken passwordAuthenticationToken = new UsernamePasswordAuthenticationToken(name, null, userDetails.getAuthorities());
        return passwordAuthenticationToken;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String tokenHeader = request.getHeader(CommonConst.JWTHEADER);
        // 如果请求头中没有Authorization信息则直接放行了
        if (tokenHeader == null || !tokenHeader.startsWith(CommonConst.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        //有jwt则需要验证
        UsernamePasswordAuthenticationToken authenticationToken = getToken(tokenHeader);
        //剩下的就交给authenticationManager、provider去做
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        super.doFilterInternal(request, response, chain);
    }
}

统一配置

以上是主要类的建立,下面我们需要加载这些配置,使得他们可以生效.新建SecurityConfig继承WebSecurityConfigurerAdapter

  • 配置拦截路劲,也就是antMatchers()方法,默认的登录“/login”和退出"/logout"是不需要配置的;另外可以配置路径具有哪些权限
  • 跨域配置
  • 加入验证和认证的filter,也就是JwtAuthenticationFilterJwtAuthenrizationFilter
@Configuration
@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled = true)开启方法级别的安全注解
public class SecurityConfig  extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("jwtUserService")
    private UserDetailsService userDetailsService;

    /**
     * 注入加密
     * @return
     */
    @Bean
    public static PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在这里指定密码的加密方式,SpringSecutity5.0之后必须指定
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        //auth.authenticationProvider(authenticationProvider());
       /* auth.inMemoryAuthentication() //认证信息存储到内存中
                .passwordEncoder(passwordEncoder())
                .withUser("zhouyu").password(passwordEncoder().encode("123456")).roles("ADMIN");*/
    }

    /**
     * 默认使用的就是DaoAuthenticationProvider,在这里只是显示的写出来参考
     * @param http
     * @throws Exception
     */
   /* @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }*/

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/image/**").permitAll()
                .antMatchers("/admin/**").hasAnyRole("ADMIN")
                .antMatchers("/article/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .csrf().disable()
                //.formLogin().disable()
                //不需要session
                .sessionManagement().disable()
                //跨域允许
                .cors()
                .and()
                .headers().addHeaderWriter(new StaticHeadersWriter(Arrays.asList(
                new Header("Access-control-Allow-Origin","*"),
                new Header("Access-Control-Expose-Headers","Authorization"))))
                .and()
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                .addFilter(new JwtAuthenrizationFilter(authenticationManager()))
                .logout()
                .addLogoutHandler(new JwtLogoutHandler())
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
                .and()
                .sessionManagement().disable();
    }

    /**
     * 跨域配置
     * @return
     */
    @Bean
    protected CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addExposedHeader("Authorization");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }
}

postman模拟测试

使用postman发出login请求,后台会返回一个jwt,我们在拿着jwt去访问首页index,验证通过会有日志显示


测试返回jwt.png

现在已经完成了验证和授权的全部,细心的你可能发现了,现实的权限管理是动态的:用户访问一个url,我们需要根据用户的权限来判断用户是否具有访问的权限.我们将在下一篇中介绍动态的权限管理如何实现.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容