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,我們需要根據用戶的權限來判斷用戶是否具有訪問的權限.我們將在下一篇中介紹動態的權限管理如何實現.

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容