??【Spring專題】「開發(fā)實(shí)戰(zhàn)」Spring Security與JWT實(shí)現(xiàn)權(quán)限管控以及登錄認(rèn)證指南

SpringSecurity介紹

SpringSecurity是一個(gè)用于Java 企業(yè)級(jí)應(yīng)用程序的安全框架尺上,主要包含用戶認(rèn)證和用戶授權(quán)兩個(gè)方面水由,相比較Shiro而言踩身,Security功能更加的強(qiáng)大较木,它可以很容易地?cái)U(kuò)展以滿足更多安全控制方面的需求侵状,但也相對(duì)它的學(xué)習(xí)成本會(huì)更高,兩種框架各有利弊正罢。實(shí)際開發(fā)中還是要根據(jù)業(yè)務(wù)和項(xiàng)目的需求來決定使用哪一種.

JWT的認(rèn)證傳輸協(xié)議

JWT是在Web應(yīng)用中安全傳遞信息的規(guī)范阵漏,從本質(zhì)上來說是Token的演變,是一種生成加密用戶身份信息的Token腺怯,特別適用于分布式單點(diǎn)登陸的場(chǎng)景袱饭,無需在服務(wù)端保存用戶的認(rèn)證信息川无,而是直接對(duì)Token進(jìn)行校驗(yàn)獲取用戶信息呛占,使單點(diǎn)登錄更為簡(jiǎn)單靈活。

系統(tǒng)搭建

環(huán)境管控

  • SpringBoot版本:2.1.6
  • SpringSecurity版本: 5.1.5
  • MyBatis-Plus版本: 3.1.0
  • JDK版本:1.8

Maven依賴如下:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--Security依賴 —>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- MybatisPlus 核心庫 —>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- 引入阿里數(shù)據(jù)庫連接池 —>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>
        <!-- StringUtilS工具 —>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>
        <!-- JSON工具 —>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.45</version>
        </dependency>
        <!-- JWT依賴 —>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>1.0.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
</dependencies>

springboot配置文件

配置如下:
# 配置端口
server:
  port: 8888
spring:
  # 配置數(shù)據(jù)源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/call_center?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
# JWT配置
jwt:
  # 密匙KEY
  secret: JWTSecret
  # HeaderKEY
  tokenHeader: Authorization
  # Token前綴字符
  tokenPrefix: callcenter-
  # 過期時(shí)間 單位秒 1天后過期=86400 7天后過期=604800
  expiration: 86400
  # 配置不需要認(rèn)證的接口
  antMatchers: /index,/login/**,/favicon.ico
# Mybatis-plus相關(guān)配置
mybatis-plus:
  # xml掃描懦趋,多個(gè)目錄用逗號(hào)或者分號(hào)分隔(告訴 Mapper 所對(duì)應(yīng)的 XML 文件位置)
  mapper-locations: classpath:mapper/*.xml
  # 以下配置均有默認(rèn)值,可以不設(shè)置
  global-config:
    db-config:
      #主鍵類型 AUTO:"數(shù)據(jù)庫ID自增" INPUT:"用戶輸入ID",ID_WORKER:"全局唯一ID (數(shù)字類型唯一ID)", UUID:"全局唯一ID UUID”;
      id-type: AUTO
      #字段策略 IGNORED:"忽略判斷"  NOT_NULL:"非 NULL 判斷")  NOT_EMPTY:"非空判斷”
      field-strategy: NOT_EMPTY
      #數(shù)據(jù)庫類型
      db-type: MYSQL
  configuration:
    # 是否開啟自動(dòng)駝峰命名規(guī)則映射:從數(shù)據(jù)庫列名到Java屬性駝峰命名的類似映射
    map-underscore-to-camel-case: true
    # 返回map時(shí)true:當(dāng)查詢數(shù)據(jù)為空時(shí)字段返回為null,false:不加這個(gè)查詢數(shù)據(jù)為空時(shí)晾虑,字段將被隱藏
    call-setters-on-nulls: true
    # 這個(gè)配置會(huì)將執(zhí)行的sql打印出來,在開發(fā)或測(cè)試的時(shí)候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

開發(fā)核心類

編寫JWT工具類

@Slf4j
public class JWTTokenUtil {

    /**
     * 生成Token
     * @Param  selfUserEntity 用戶安全實(shí)體
     * @Return Token
     */
    public static String createAccessToken(SelfUserEntity selfUserEntity){
        // 登陸成功生成JWT
        String token = Jwts.builder()
                // 放入用戶名和用戶ID
                .setId(selfUserEntity.getUserId()+””)
                // 主題
                .setSubject(selfUserEntity.getUsername())
                // 簽發(fā)時(shí)間
                .setIssuedAt(new Date())
                // 簽發(fā)者
                .setIssuer(“sans”)
                // 自定義屬性 放入用戶擁有權(quán)限
                .claim("authorities", JSON.toJSONString(selfUserEntity.getAuthorities()))
                // 失效時(shí)間
                .setExpiration(new Date(System.currentTimeMillis() + JWTConfig.expiration))
                // 簽名算法和密鑰
                .signWith(SignatureAlgorithm.HS512, JWTConfig.secret)
                .compact();
        return token;
    }
}

無權(quán)限處理類

@Component
public class UserAuthAccessDeniedHandler implements AccessDeniedHandler{
    /**
     * 暫無權(quán)限返回結(jié)果
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception){
        ResultUtil.responseJson(response,ResultUtil.resultCode(403,"未授權(quán)”));
    }
}

用戶未登錄處理類

/**
 * 用戶未登錄處理類
 */
@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint{
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
        ResultUtil.responseJson(response,ResultUtil.resultCode(401,"未登錄”));
    }
}

登錄失敗處理類

@Slf4j
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
    /**
     * 登錄失敗返回結(jié)果
     * @Author Sans
     * @CreateTime 2019/10/3 9:12
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
        // 這些對(duì)于操作的處理類可以根據(jù)不同異常進(jìn)行不同處理
        if (exception instanceof UsernameNotFoundException){
            log.info("【登錄失敗】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用戶名不存在”));
        }
        if (exception instanceof LockedException){
            log.info("【登錄失敗】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用戶被凍結(jié)”));
        }
        if (exception instanceof BadCredentialsException){
            log.info("【登錄失敗】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用戶名密碼不正確”));
        }
        ResultUtil.responseJson(response,ResultUtil.resultCode(500,"登錄失敗”));
    }
}

登錄成功處理類

@Slf4j
@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        // 組裝JWT
        SelfUserEntity selfUserEntity =  (SelfUserEntity) authentication.getPrincipal();
        String token = JWTTokenUtil.createAccessToken(selfUserEntity);
        token = JWTConfig.tokenPrefix + token;
        // 封裝返回參數(shù)
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200”);
        resultData.put("msg", "登錄成功”);
        resultData.put("token",token);
        ResultUtil.responseJson(response,resultData);
    }
}

登出成功處理類

@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200”);
        resultData.put("msg", "登出成功”);
        SecurityContextHolder.clearContext();
        ResultUtil.responseJson(response,ResultUtil.resultSuccess(resultData));
    }
}

編寫Security核心類

自定義登錄驗(yàn)證
@Component
public class UserAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private SelfUserDetailsService selfUserDetailsService;
    @Autowired
    private SysUserService sysUserService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 獲取表單輸入中返回的用戶名
        String userName = (String) authentication.getPrincipal();
        // 獲取表單中輸入的密碼
        String password = (String) authentication.getCredentials();
        // 查詢用戶是否存在
        SelfUserEntity userInfo = selfUserDetailsService.loadUserByUsername(userName);
        if (userInfo == null) {
            throw new UsernameNotFoundException("用戶名不存在”);
        }
        // 還要判斷密碼是否正確仅叫,這里我們的密碼使用BCryptPasswordEncoder進(jìn)行加密的
        if (!new BCryptPasswordEncoder().matches(password, userInfo.getPassword())) {
            throw new BadCredentialsException("密碼不正確”);
        }
        // 還可以加一些其他信息的判斷,比如用戶賬號(hào)已停用等判斷
        if (userInfo.getStatus().equals("PROHIBIT”)){
            throw new LockedException("該用戶已被凍結(jié)”);
        }
        // 角色集合
        Set<GrantedAuthority> authorities = new HashSet<>();
        // 查詢用戶角色
        List<SysRoleEntity> sysRoleEntityList = sysUserService.selectSysRoleByUserId(userInfo.getUserId());
        for (SysRoleEntity sysRoleEntity: sysRoleEntityList){
            authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getRoleName()));
        }
        userInfo.setAuthorities(authorities);
        // 進(jìn)行登錄
        return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
    }
    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}
自定義PermissionEvaluator注解驗(yàn)證
自定義權(quán)限注解驗(yàn)證
@Component
public class UserPermissionEvaluator implements PermissionEvaluator {
    @Autowired
    private SysUserService sysUserService;
    /**
     * hasPermission鑒權(quán)方法
     * 這里僅僅判斷PreAuthorize注解中的權(quán)限表達(dá)式
     * 實(shí)際中可以根據(jù)業(yè)務(wù)需求設(shè)計(jì)數(shù)據(jù)庫通過targetUrl和permission做更復(fù)雜鑒權(quán)
     * @Param  authentication  用戶身份
     * @Param  targetUrl  請(qǐng)求路徑
     * @Param  permission 請(qǐng)求路徑權(quán)限
     * @Return boolean 是否通過
     */
    @Override
    public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
        // 獲取用戶信息
        SelfUserEntity selfUserEntity =(SelfUserEntity) authentication.getPrincipal();
        // 查詢用戶權(quán)限(這里可以將權(quán)限放入緩存中提升效率)
        Set<String> permissions = new HashSet<>();
        List<SysMenuEntity> sysMenuEntityList = sysUserService.selectSysMenuByUserId(selfUserEntity.getUserId());
        for (SysMenuEntity sysMenuEntity:sysMenuEntityList) {
            permissions.add(sysMenuEntity.getPermission());
        }
        // 權(quán)限對(duì)比
        if (permissions.contains(permission.toString())){
            return true;
        }
        return false;
    }
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

SpringSecurity核心配置類

/**
 * SpringSecurity核心配置類
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //開啟權(quán)限注解,默認(rèn)是關(guān)閉的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定義登錄成功處理器
     */
    @Autowired
    private UserLoginSuccessHandler userLoginSuccessHandler;
    /**
     * 自定義登錄失敗處理器
     */
    @Autowired
    private UserLoginFailureHandler userLoginFailureHandler;
    /**
     * 自定義注銷成功處理器
     */
    @Autowired
    private UserLogoutSuccessHandler userLogoutSuccessHandler;
    /**
     * 自定義暫無權(quán)限處理器
     */
    @Autowired
    private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
    /**
     * 自定義未登錄的處理器
     */
    @Autowired
    private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
    /**
     * 自定義登錄邏輯驗(yàn)證器
     */
    @Autowired
    private UserAuthenticationProvider userAuthenticationProvider;
    
    /**
     * 加密方式
     * @CreateTime 2019/10/1 14:00
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**
     * 注入自定義PermissionEvaluator
     */
    @Bean
    public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(new UserPermissionEvaluator());
        return handler;
    }
    /**
     * 配置登錄驗(yàn)證邏輯
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth){
        //這里可啟用我們自己的登陸驗(yàn)證邏輯
        auth.authenticationProvider(userAuthenticationProvider);
    }
    /**
     * 配置security的控制邏輯
     * @Author Sans
     * @CreateTime 2019/10/1 16:56
     * @Param  http 請(qǐng)求
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //不進(jìn)行權(quán)限驗(yàn)證的請(qǐng)求或資源(從配置文件中讀取)
               .antMatchers(JWTConfig.antMatchers.split(",")).permitAll()
                //其他的需要登陸后才能訪問
                .anyRequest().authenticated()
                .and()
                //配置未登錄自定義處理類
                .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
                .and()
                //配置登錄地址
                .formLogin()
                .loginProcessingUrl("/login/userLogin”)
                //配置登錄成功自定義處理類
                .successHandler(userLoginSuccessHandler)
                //配置登錄失敗自定義處理類
                .failureHandler(userLoginFailureHandler)
                .and()
                //配置登出地址
                .logout()
                .logoutUrl("/login/userLogout”)
                //配置用戶登出自定義處理類
                .logoutSuccessHandler(userLogoutSuccessHandler)
                .and()
                //配置沒有權(quán)限自定義處理類
                .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
                .and()
                // 取消跨站請(qǐng)求偽造防護(hù)
                .csrf().disable();
        // 基于Token不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 禁用緩存
        http.headers().cacheControl();
        // 添加JWT過濾器
        http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));
    }
}

編寫JWT攔截類

編寫JWT接口請(qǐng)求校驗(yàn)攔截器
/**
 * JWT接口請(qǐng)求校驗(yàn)攔截器
 * 請(qǐng)求接口時(shí)會(huì)進(jìn)入這里驗(yàn)證Token是否合法和過期
 */
@Slf4j
public class JWTAuthenticationTokenFilter extends BasicAuthenticationFilter {

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

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 獲得TokenHeader
        String tokenHeader = request.getHeader(JWTConfig.tokenHeader);
        if (null!=tokenHeader && tokenHeader.startsWith(JWTConfig.tokenPrefix)) {
            try {
                // 獲取請(qǐng)求頭中JWT的Token
                if (!StringUtils.isEmpty(request.getHeader(JWTConfig.tokenHeader))) {
                    // 截取JWT前綴
                    String token = request.getHeader(JWTConfig.tokenHeader).replace(JWTConfig.tokenPrefix, “”);
                    // 解析JWT
                    Claims claims = Jwts.parser()
                            .setSigningKey(JWTConfig.secret)
                            .parseClaimsJws(token)
                            .getBody();
                    // 獲取用戶名
                    String username = claims.getSubject();
                    String userId=claims.getId();
                    if(!StringUtils.isEmpty(username)&&!StringUtils.isEmpty(userId)) {
                        // 獲取角色
                        List<GrantedAuthority> authorities = new ArrayList<>();
                        String authority = claims.get("authorities").toString();
                        if(!StringUtils.isEmpty(authority)){
                            List<Map<String,String>> authorityMap = JSONObject.parseObject(authority, List.class);
                            for(Map<String,String> role : authorityMap){
                                if(!StringUtils.isEmpty(role)) {
                                    authorities.add(new SimpleGrantedAuthority(role.get("authority")));
                                }
                            }
                        }
                        //組裝參數(shù)
                        SelfUserEntity selfUserEntity = new SelfUserEntity();
                        selfUserEntity.setUsername(claims.getSubject());
                        selfUserEntity.setUserId(Long.parseLong(claims.getId()));
                        selfUserEntity.setAuthorities(authorities);
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(selfUserEntity, userId, authorities);
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            } catch (ExpiredJwtException e){
                log.info("Token過期”);
            } catch (Exception e) {
                log.info("Token無效”);
            }
        }
        filterChain.doFilter(request, response);
        return;
    }
}

權(quán)限注解和hasPermission權(quán)限擴(kuò)展

Security允許我們?cè)诙xURL方法訪問所應(yīng)有的注解權(quán)限時(shí)使用SpringEL表達(dá)式,在定義所需的訪問權(quán)限時(shí)如果對(duì)應(yīng)的表達(dá)式返回結(jié)果為true忌穿,則表示擁有對(duì)應(yīng)的權(quán)限刃唤,反之則沒有權(quán)限,會(huì)進(jìn)入到我們配置的UserAuthAccessDeniedHandler(暫無權(quán)限處理類)中進(jìn)行處理.這里舉一些例子,代碼中注釋有對(duì)應(yīng)的描述.

image
    /**
     * 管理端信息
     * @Return Map<String,Object> 返回?cái)?shù)據(jù)MAP
     */
    @PreAuthorize("hasRole('ADMIN’)”)
    @RequestMapping(value = "/info",method = RequestMethod.GET)
    public Map<String,Object> userLogin(){
        Map<String,Object> result = new HashMap<>();
        SelfUserEntity userDetails = SecurityUtil.getUserInfo();
        result.put("title","管理端信息”);
        result.put("data",userDetails);
        return ResultUtil.resultSuccess(result);
    }
    /**
     * 擁有ADMIN或者USER角色可以訪問
     * @Return Map<String,Object> 返回?cái)?shù)據(jù)MAP
     */
    @PreAuthorize("hasAnyRole('ADMIN','USER’)”)
    @RequestMapping(value = "/list",method = RequestMethod.GET)
    public Map<String,Object> list(){
        Map<String,Object> result = new HashMap<>();
        List<SysUserEntity> sysUserEntityList = sysUserService.list();
        result.put("title","擁有用戶或者管理員角色都可以查看”);
        result.put("data",sysUserEntityList);
        return ResultUtil.resultSuccess(result);
    }
    /**
     * 擁有ADMIN和USER角色可以訪問
     * @Return Map<String,Object> 返回?cái)?shù)據(jù)MAP
     */
    @PreAuthorize("hasRole('ADMIN') and hasRole('USER’)”)
    @RequestMapping(value = "/menuList",method = RequestMethod.GET)
    public Map<String,Object> menuList(){
        Map<String,Object> result = new HashMap<>();
        List<SysMenuEntity> sysMenuEntityList = sysMenuService.list();
        result.put("title","擁有用戶和管理員角色都可以查看”);
        result.put("data",sysMenuEntityList);
        return ResultUtil.resultSuccess(result);
    }

通常情況下使用hasRole和hasAnyRole基本可以滿足大部分鑒權(quán)需求坎缭,但是有時(shí)候面對(duì)更復(fù)雜的場(chǎng)景上述常規(guī)表示式無法完成權(quán)限認(rèn)證竟痰,Security也為我們提供了解決方案签钩。通過hasPermission()來擴(kuò)展表達(dá)式,使用hasPermission()坏快,首先要實(shí)現(xiàn)PermissionEvaluator接口

自定義權(quán)限注解驗(yàn)證

@Component
public class UserPermissionEvaluator implements PermissionEvaluator {
    @Autowired
    private SysUserService sysUserService;
    /**
     * hasPermission鑒權(quán)方法
     * 這里僅僅判斷PreAuthorize注解中的權(quán)限表達(dá)式
     * 實(shí)際中可以根據(jù)業(yè)務(wù)需求設(shè)計(jì)數(shù)據(jù)庫通過targetUrl和permission做更復(fù)雜鑒權(quán)
     * 當(dāng)然targetUrl不一定是URL可以是數(shù)據(jù)Id還可以是管理員標(biāo)識(shí)等,這里根據(jù)需求自行設(shè)計(jì)
     * @Param  authentication  用戶身份(在使用hasPermission表達(dá)式時(shí)Authentication參數(shù)默認(rèn)會(huì)自動(dòng)帶上)
     * @Param  targetUrl  請(qǐng)求路徑
     * @Param  permission 請(qǐng)求路徑權(quán)限
     * @Return boolean 是否通過
     */
    @Override
    public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
        // 獲取用戶信息
        SelfUserEntity selfUserEntity =(SelfUserEntity) authentication.getPrincipal();
        // 查詢用戶權(quán)限(這里可以將權(quán)限放入緩存中提升效率)
        Set<String> permissions = new HashSet<>();
        List<SysMenuEntity> sysMenuEntityList = sysUserService.selectSysMenuByUserId(selfUserEntity.getUserId());
        for (SysMenuEntity sysMenuEntity:sysMenuEntityList) {
            permissions.add(sysMenuEntity.getPermission());
        }
        // 權(quán)限對(duì)比
        if (permissions.contains(permission.toString())){
            return true;
        }
        return false;
    }
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

請(qǐng)求方法上添加hasPermission示例

    /**
     * 擁有sys:user:info權(quán)限可以訪問
     * hasPermission 第一個(gè)參數(shù)是請(qǐng)求路徑 第二個(gè)參數(shù)是權(quán)限表達(dá)式
     * @Return Map<String,Object> 返回?cái)?shù)據(jù)MAP
     */
    @PreAuthorize("hasPermission('/admin/userList','sys:user:info’)”)
    @RequestMapping(value = "/userList",method = RequestMethod.GET)
    public Map<String,Object> userList(){
        Map<String,Object> result = new HashMap<>();
        List<SysUserEntity> sysUserEntityList = sysUserService.list();
        result.put("title","擁有sys:user:info權(quán)限都可以查看”);
        result.put("data",sysUserEntityList);
        return ResultUtil.resultSuccess(result);
    }

hasPermission可以也可以和其他表達(dá)式聯(lián)合使用

    /**
     * 擁有ADMIN角色和sys:role:info權(quán)限可以訪問
     * @Author Sans
     * @CreateTime 2019/10/2 14:22
     * @Return Map<String,Object> 返回?cái)?shù)據(jù)MAP
     */
    @PreAuthorize("hasRole('ADMIN') and hasPermission('/admin/adminRoleList','sys:role:info’)”)
    @RequestMapping(value = "/adminRoleList",method = RequestMethod.GET)
    public Map<String,Object> adminRoleList(){
        Map<String,Object> result = new HashMap<>();
        List<SysRoleEntity> sysRoleEntityList = sysRoleService.list();
        result.put("title","擁有ADMIN角色和sys:role:info權(quán)限可以訪問”);
        result.put("data",sysRoleEntityList);
        return ResultUtil.resultSuccess(result);
    }

測(cè)試

創(chuàng)建賬戶這里用戶加密使用了Security推薦的bCryptPasswordEncoder方法

注冊(cè)用戶操作

    @Test
    public void contextLoads() {
        // 注冊(cè)用戶
        SysUserEntity sysUserEntity = new SysUserEntity();
        sysUserEntity.setUsername("sans”);
        sysUserEntity.setPassword(bCryptPasswordEncoder.encode("123456”));
        // 設(shè)置用戶狀態(tài)
        sysUserEntity.setStatus("NORMAL”);
        sysUserService.save(sysUserEntity);
        // 分配角色 1:ADMIN 2:USER
        SysUserRoleEntity sysUserRoleEntity = new SysUserRoleEntity();
        sysUserRoleEntity.setRoleId(2L);
        sysUserRoleEntity.setUserId(sysUserEntity.getUserId());
        sysUserRoleService.save(sysUserRoleEntity);
    }

登錄USER角色賬號(hào),登錄成功后我們會(huì)獲取到身份認(rèn)證的Token铅檩。

登錄用戶操作

image

訪問USER角色的接口,把上一步獲取到的Token設(shè)置在Headers中莽鸿,Key為Authorization,我們之前實(shí)現(xiàn)的JWTAuthenticationTokenFilter攔截器會(huì)根據(jù)請(qǐng)求頭中的Authorization獲取并解析Token昧旨。

查看用戶操作

image
image

使用USER角色Token訪問ADMIN角色的接口,會(huì)被拒絕,告知未授權(quán)(暫無權(quán)限會(huì)進(jìn)入我們定義的UserAuthAccessDeniedHandler這個(gè)類進(jìn)行處理)

更換ADMIN角色進(jìn)行登錄并訪問ADMIN接口

查看用戶操作

image

非主線篇

本文講述一下如何自定義Spring Security的登錄認(rèn)證操作,目前大多數(shù)的項(xiàng)目都是基于前后端分離的祥得,但是也有很多場(chǎng)景下也會(huì)存在后端模板技術(shù)兔沃,如何給出一個(gè)采用ajax的登錄及返回的前后端分離方式并且兼容后端模板技術(shù)的功能代碼。

AjaxAuthSuccessHandler

public class AjaxAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_OK);
    }
}

AjaxAuthFailHandler

public class AjaxAuthFailHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed”);
    }
}

ajax的異常處理

public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        if(isAjaxRequest(request)){
           response.sendError(HttpServletResponse.SC_UNAUTHORIZED,authException.getMessage());
        }else{
            response.sendRedirect("/login.html”);
        }

    }

    public static boolean isAjaxRequest(HttpServletRequest request) {
        String ajaxFlag = request.getHeader("X-Requested-With”);
        return ajaxFlag != null && "XMLHttpRequest".equals(ajaxFlag);
    }
}

這里我們自定義成功及失敗的ajax返回级及,當(dāng)然這里我們簡(jiǎn)單處理粘拾,只返回statusCode

security配置

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .exceptionHandling().authenticationEntryPoint(new UnauthorizedEntryPoint())
                .and()
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/login","/css/**", "/js/**","/fonts/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html”)
                .loginProcessingUrl("/login”)
                .usernameParameter(“name”)
                .passwordParameter(“password”)
                .successHandler(new AjaxAuthSuccessHandler())
                .failureHandler(new AjaxAuthFailHandler())
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout”)
                .permitAll();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("admin").password("admin").roles("USER”);
    }
}

這里有幾個(gè)要注意的點(diǎn):

  • permitAll:這里要添加前端資源路徑,以及登陸表單請(qǐng)求的接口地址/login

  • loginPage:這里設(shè)置登錄頁面的地址创千,這里我們用靜態(tài)頁面缰雇,即static目錄下的login.html

  • ajax配置:將authenticationEntryPoint,successHandler追驴,failureHandler設(shè)置為上面自定義的ajax處理類

資料參考

https://segmentfault.com/a/1190000012140889

https://segmentfault.com/a/1190000010672041

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末械哟,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子殿雪,更是在濱河造成了極大的恐慌暇咆,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丙曙,死亡現(xiàn)場(chǎng)離奇詭異爸业,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)亏镰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門扯旷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人索抓,你說我怎么就攤上這事钧忽。” “怎么了逼肯?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵耸黑,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我篮幢,道長(zhǎng)大刊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任三椿,我火速辦了婚禮缺菌,結(jié)果婚禮上曲尸,老公的妹妹穿的比我還像新娘。我一直安慰自己男翰,他們只是感情好另患,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛾绎,像睡著了一般昆箕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上租冠,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天鹏倘,我揣著相機(jī)與錄音,去河邊找鬼顽爹。 笑死纤泵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的镜粤。 我是一名探鬼主播捏题,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼肉渴!你這毒婦竟也來了公荧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤同规,失蹤者是張志新(化名)和其女友劉穎循狰,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體券勺,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绪钥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了关炼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片程腹。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖盗扒,靈堂內(nèi)的尸體忽然破棺而出跪楞,到底是詐尸還是另有隱情侣灶,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布褥影,位于F島的核電站寨典,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜躺枕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一娩缰、第九天 我趴在偏房一處隱蔽的房頂上張望器虾。 院中可真熱鬧,春花似錦抚垃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吃挑,卻和暖如春钝荡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背舶衬。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工埠通, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逛犹。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓端辱,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親虽画。 傳聞我的和親對(duì)象是個(gè)殘疾皇子掠手,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容