電商項目實戰(架構四)——SpringSecurity + JWT實現認證與授權進行用戶登錄


一、前言

  登錄和授權模塊是整個項目的安全鎖,是開發中的必要一環。本文通過使用SpringSecurity安全框架和JWT實現后台的登錄和授權,為了方便接口的在線測試,對swagger-ui的配置文件進行改造,使其能夠拿到登錄令牌token。

二、介紹

  1、SpringSecurity

  SpringSecurity是一個高性能的認證與授權的框架,為java項目提供了認證和授權的功能。

  2、JWT

  JWT是JSON WEB TOKEN的縮寫,它是基於RFC 7519標准定義的一種可以安全傳輸的JSON對象,由於使用了數字簽名,所以是安全可信任的。

  JWT的完全體是token,token的組成分為三部分:header,payload,signature;

  header存放的是生成簽名的算法標准

{"alg":"HS512"}

  payload是驗證主體,存放用戶名,token的創建時間和過期時間

{"sub":"admin","created":"1574405893304","end":"1575010693"}

  signature是以header和payload為主體生成的簽名驗證,一旦header或payload被篡改,簽名將驗證失敗。

String signature = HMACSHA512(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret);

  JWT實例

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NzQ0MDU4OTMzMDQsImV4cCI6MTU3NTAxMDY5M30.TOM6eciLWaeDPUV5Q1CfISzN0H1-4TYCriKD2FOkcznHpkoJPDID105xdG0D_vlGlx0FweGvoMphISga7VAUmw

  解析JWT,進入:https://jwt.io/

  

三、實現認證與授權的流程說明

  1、用戶調用登錄接口進行登錄,成功后獲取到生成的token;

  2、之后用戶每次調用接口都會在http的header請求頭中添加key為Authorization,值為獲取到的token;

  3、后台過濾器會解析請求頭中Authorization的值token,校驗數字簽名,獲取封裝的用戶信息,實現認證與授權。

四、Hutool工具包

  Hutool工具包可以幫助開發者簡化代碼,優化代碼。

五、mysql新建數據庫表

  1、ums_admin:用戶表

  

  2、ums_role:用戶角色表

  

  3、ums_permission:用戶權限表

  

  4、ums_admin_role_relation:用戶角色關系表

  

  5、ums_role_permission_relation:角色權限關系表

  

  6、ums_admin_permission_relation:用戶權限關系表

  

  7、mybatis逆向生成相應的model,example,mapper,mapper.xml

  resource包下修改generatorConfig.xml文件,添加生成表名

<!--生成全部表tableName設為%-->

        <!--商品品牌表-->
        <table tableName="pms_brand"></table>
        <!--用戶表-->
        <table tableName="ums_admin"></table>
        <!--角色表-->
        <table tableName="ums_role"></table>
        <!--權限表-->
        <table tableName="ums_permission"></table>
        <!--用戶角色關系表-->
        <table tableName="ums_admin_role_relation"></table>
        <!--角色權限關系表-->
        <table tableName="ums_role_permission_relation"></table>
        <!--用戶權限關系表-->
        <table tableName="ums_admin_permission_relation"></table>

  運行mbg包下Generator程序,自動生成代碼

  

六、項目整合SpringSecurity安全框架

  1、添加相關pom.xml依賴配置

<!--SpringSecurity依賴配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--hutool工具包依賴配置-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.7</version>
        </dependency>
        <!--JWT依賴配置-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

  2、在application.yml中添加自定義jwt配置

#項目啟動端口
server:
  port: 10077
spring:
  #連接mysql數據庫
  datasource:
    url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
  #連接redis緩存數據庫
  redis:
    host: localhost    #redis服務器地址
    database: 0        #redis數據庫索引(默認為0)
    port: 6379         #redis服務器連接端口
    password:          #redis連接密碼(默認為空)
    #redis連接池
    jedis:
      pool:
        max-wait: -1ms  #連接池最大阻塞等待時間(使用負值表示沒有限制)
        min-idle: 0     #連接池中的最小空閑連接
        max-idle: 8     #連接池中的最大空閑連接
        max-active: 8   #連接池最大連接數(使用負值表示沒有限制)
    timeout: 3000ms     #連接超時時間(毫秒)

#redis自定義配置
redis:
  key:
    prefix:
      authCode: "portal:authCode:"
    expire:
      authCode: 120

#mybatis映射xml文件路徑
mybatis:
  mapper-locations:
    classpath: com/zzb/test/admin/*/*.xml  

#jwt自定義配置
jwt:
  tokenHeader: Authorization  #JWT存儲的請求頭
  secret: mySecret            #JWT加解密使用的密鑰
  expiration: 604800          #JWT的超期時間(60*60*24)
  tokenHead: Bearer           #JWT負載中拿到開頭

  3、新建存放通用工具類的包utils,在utils包下新建JWT的通用工具類

  

   JwtTokenUtil類

package com.zzb.test.admin.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken的生成工具類
 * Created by zzb on 2019/11/21 10:23
 */
@Component
public class JwtTokenUtil {
    private static final Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 封裝用戶信息,並生成token
     * @param userDetails
     * @return
     */
    public String generateToken(UserDetails userDetails){
        Map<String,Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED,new Date());
        return this.generateTokenByClaims(claims);
    }

    /**
     * 根據負載生成token
     * @param claims
     * @return
     */
    public String generateTokenByClaims(Map<String,Object> claims){
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(this.generationExpirationDate())
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     * 解析token,獲取負載主體
     * @param token
     * @return
     */
    public Claims getClaimsFromToken(String token){
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            logger.info("JWT格式驗證失敗:{}",token);
        }

        return claims;
    }

    /**
     * 解析token,獲取負載主體中的用戶名
     * @param token
     * @return
     */
    public String getUserNameFromToken(String token){
        String username = null;
        try {
            Claims claims = this.getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            logger.info("JWT解析token失敗:{}",token);
        }

        return username;
    }

    /**
     * 生成token的過期時間
     * @return
     */
    public Date generationExpirationDate(){
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 驗證token是否有效
     * @param token
     * @param userDetails
     * @return
     */
    public boolean validateToken(String token, UserDetails userDetails){
        String username = this.getUserNameFromToken(token);
        if (StringUtils.isEmpty(username)) {
            return false;
        }
        if (username.equals(userDetails.getUsername()) && this.isTokenExpired(token)) {
            return true;
        }

        return false;
    }

    /**
     * 驗證token是否過期
     * @param token
     * @return
     */
    public boolean isTokenExpired(String token){
        Claims claims = this.getClaimsFromToken(token);
        Date expired = claims.getExpiration();
        return new Date().before(expired);
    }

    /**
     * 刷新token
     * @param token
     * @return
     */
    public String refreshToken(String token){
        if (!this.isTokenExpired(token)) {
            logger.info("token已過期:{}",token);
            return null;
        } else {
            Claims claims = this.getClaimsFromToken(token);
            claims.replace(CLAIM_KEY_CREATED,new Date());
            return this.generateTokenByClaims(claims);
        }
    }


}

  4、在通用包common下新建未登錄時返回結果類RestAuthenestAuthenticationEntryPoint

package com.zzb.test.admin.common;

import cn.hutool.json.JSONUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 當未登錄或者token失效訪問接口時,自定義的返回結果
 * Created by zzb on 2019/11/21 14:22
 */
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.failed(e.getMessage())));
        response.getWriter().flush();
    }
}

  在通用包common下新建無權訪問時返回結果類RestfulAccessDeinidccessDeniedHandler

package com.zzb.test.admin.common;

import cn.hutool.json.JSONUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 當訪問接口沒有權限時,自定義的返回結果
 * Created by zzb on 2019/11/21 14:15
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.forbbiden(e.getMessage())));
        response.getWriter().flush();
    }
}

  在通用包common下新建JWT登錄授權過濾器JwtAuthenticationTokenFilter

package com.zzb.test.admin.common;

import com.zzb.test.admin.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * JWT登錄授權過濾器
 * Created by zzb on 2019/11/21 14:31
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            logger.info("解析token獲取到用戶名:{}",username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken,userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    logger.info("給用戶授權:{}",username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }

        chain.doFilter(request,response);
    }
}

  5、在service包下新建UmsAdminService接口

package com.zzb.test.admin.service;

import com.zzb.test.admin.mbg.model.UmsAdmin;
import com.zzb.test.admin.mbg.model.UmsPermission;

import java.util.List;

/**
 * 用戶管理Service
 * Created by zzb on 2019/11/21 17:33
 */
public interface UmsAdminService {
    /**
     * 根據用戶名查詢用戶
     * @param username
     * @return
     */
    UmsAdmin getAdminByUsername(String username);

    /**
     * 用戶注冊
     * @param umsAdminParam
     * @return
     */
    UmsAdmin register(UmsAdmin umsAdminParam);

    /**
     * 用戶登錄
     * @param username
     * @param password
     * @return
     */
    String login(String username,String password);

    /**
     * 根據用戶id查詢用戶權限
     * @param adminId
     * @return
     */
    List<UmsPermission> getPermissionList(Long adminId);
}

  在impl包下新建其實現類UmsAdminServiceImpl

package com.zzb.test.admin.service.impl;

import cn.hutool.core.lang.Assert;
import com.zzb.test.admin.dao.UmsAdminRoleRelationDao;
import com.zzb.test.admin.mbg.mapper.UmsAdminMapper;
import com.zzb.test.admin.mbg.model.UmsAdmin;
import com.zzb.test.admin.mbg.model.UmsAdminExample;
import com.zzb.test.admin.mbg.model.UmsPermission;
import com.zzb.test.admin.service.UmsAdminService;
import com.zzb.test.admin.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.Date;
import java.util.List;

/**
 * 用戶管理Service的impl
 * Created by zzb on 2019/11/21 17:41
 */
@Service
@Transactional
public class UmsAdminServiceImpl implements UmsAdminService {

    private static final Logger logger = LoggerFactory.getLogger(UmsAdminServiceImpl.class);
    @Autowired
    private UmsAdminMapper umsAdminMapper;
    @Autowired
    private UmsAdminRoleRelationDao umsAdminRoleRelationDao;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public UmsAdmin getAdminByUsername(String username) {
        UmsAdminExample uae = new UmsAdminExample();
        uae.createCriteria().andUsernameEqualTo(username)
                .andDelStatusEqualTo("0")
                .andStatusEqualTo(true);
        List<UmsAdmin> admins = umsAdminMapper.selectByExample(uae);
        Assert.notNull(admins,"用戶名不存在");
        return admins.get(0);
    }

    @Override
    public List<UmsPermission> getPermissionList(Long adminId) {
        return umsAdminRoleRelationDao.getPermissionList(adminId);
    }

    @Override
    public UmsAdmin register(UmsAdmin umsAdminParam) {
        //是否有相同用戶名
        UmsAdminExample uae = new UmsAdminExample();
        uae.createCriteria().andUsernameEqualTo(umsAdminParam.getUsername())
                .andDelStatusEqualTo("0");
        List list = umsAdminMapper.selectByExample(uae);
        if (!CollectionUtils.isEmpty(list)) {
            return null;
        }

        umsAdminParam.setStatus(true);
        umsAdminParam.setDelStatus("0");
        umsAdminParam.setCreateTime(new Date());
        umsAdminParam.setModifyTime(new Date());
        //將密碼進行加密
        String encodePassword = passwordEncoder.encode(umsAdminParam.getPassword());
        umsAdminParam.setPassword(encodePassword);
        if (umsAdminMapper.insert(umsAdminParam)<1) {
            return null;
        };
        return umsAdminParam;
    }

    @Override
    public String login(String username, String password) {
        String token = null;
        try {
            //校驗用戶名,封裝用戶實體
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            //檢驗密碼
            if (!passwordEncoder.matches(password,userDetails.getPassword())) {
                throw new BadCredentialsException("密碼錯誤");
            }
            //根據正確的用戶名密碼生成token
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            token = jwtTokenUtil.generateToken(userDetails);
        } catch (AuthenticationException e) {
            logger.warn("登錄異常{}",e.getMessage());
        }

        return token;
    }
}

  需要自定義寫sql語句獲取權限,新建dao包,並在dao包下新建UmsAdminRoleRelationDao

package com.zzb.test.admin.dao;

import com.zzb.test.admin.mbg.model.UmsPermission;

import java.util.List;

/**
 * 用戶與角色關系自定義dao
 * Created by zzb on 2019/11/21 18:00
 */
public interface UmsAdminRoleRelationDao {
    /**
     * 獲取用戶的所有權限
     * @param adminId
     * @return
     */
    List<UmsPermission> getPermissionList(Long adminId);
}

  新建對應的xml文件UmsAdminRoleRelationDao.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzb.test.admin.dao.UmsAdminRoleRelationDao">
  <select id="getPermissionList" resultMap="com.zzb.test.admin.mbg.mapper.UmsPermissionMapper.BaseResultMap" parameterType="java.lang.Long">
    SELECT
      p.*
    FROM
      ums_admin_role_relation ar
    LEFT JOIN ums_role r ON ar.role_id = r.id
    LEFT JOIN ums_role_permission_relation rp ON r.id = rp.role_id
    LEFT JOIN ums_permission p ON rp.permission_id = p.id
    WHERE
      ar.admin_id = #{adminId}
    AND p.id IS NOT NULL
    AND p.id NOT IN (
      SELECT
        p.id
      FROM
        ums_admin_permission_relation pr
      LEFT JOIN ums_permission p ON pr.permission_id = p.id
      WHERE 1=1
      AND pr.admin_id = #{adminId}
    )
    UNION
    SELECT
      p.*
    FROM
      ums_admin_permission_relation pr
    LEFT JOIN ums_permission p ON pr.permission_id = p.id
    WHERE 1=1
    AND pr.admin_id = #{adminId}
  </select>
</mapper>

  修改mybatis的映射配置類MybatisConfig

package com.zzb.test.admin.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

/**
 * Created by zzb on 2019/11/15 9:36
 */
@Configuration
@MapperScan(basePackages = {"com.zzb.test.admin.mbg.mapper","com.zzb.test.admin.dao"})
public class MybatisConfig {
}

  6、新建自定義實體包dto,並新建SpringSecurity需要的用戶實體AdminUserDetails

package com.zzb.test.admin.dto;

import com.zzb.test.admin.mbg.model.UmsAdmin;
import com.zzb.test.admin.mbg.model.UmsPermission;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * SpringSecurity需要的用戶詳情
 * Created by zzb on 2019/11/21 18:22
 */
public class AdminUserDetails implements UserDetails {

    private UmsAdmin umsAdmin;
    private List<UmsPermission> permissionList;

    public AdminUserDetails(UmsAdmin umsAdmin,List<UmsPermission> permissionList){
        this.umsAdmin = umsAdmin;
        this.permissionList = permissionList;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //返回當前用戶的權限
        return permissionList.stream()
                .filter(permission -> permission.getPerValue()!=null)
                .map(permission -> new SimpleGrantedAuthority(permission.getPerValue()))
                .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return umsAdmin.getPassword();
    }

    @Override
    public String getUsername() {
        return umsAdmin.getUsername();
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return umsAdmin.getStatus();
    }
}

  7、在config包下新建SpringSecurity的配置類SecturyConfig

package com.zzb.test.admin.config;

import com.zzb.test.admin.common.JwtAuthenticationTokenFilter;
import com.zzb.test.admin.common.RestAuthenticationEntryPoint;
import com.zzb.test.admin.common.RestfulAccessDeniedHandler;
import com.zzb.test.admin.dto.AdminUserDetails;
import com.zzb.test.admin.mbg.model.UmsAdmin;
import com.zzb.test.admin.mbg.model.UmsPermission;
import com.zzb.test.admin.service.UmsAdminService;
import io.jsonwebtoken.lang.Assert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.util.List;

/**
 * SpringSecurity的配置類
 * Created by zzb on 2019/11/21 13:37
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UmsAdminService umsAdminService;
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    /**
     * 配置需要攔截的url路徑、jwt過濾器及出異常后的處理器
     * @param httpSecurity
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET,        //允許對於網站靜態資源的無授權訪問
                        "/", "/*.html","favicon.icon","/**/*.html","/**/*.css","/**/*.js",
                        "/swagger-resources/**","/v2/api-docs/**")
                .permitAll()
                .antMatchers("/admin/login","/admin/register")      //對登錄注冊允許匿名訪問
                .permitAll()
                .antMatchers(HttpMethod.OPTIONS)        //跨域請求會先進行一次options請求
                .permitAll()
//                .antMatchers("/**")     //測試時全部放開
//                .permitAll()
                .anyRequest()       //除上面外的所有請求全部需要鑒權認證
                .authenticated();
        //禁用緩存
        httpSecurity.headers().cacheControl();
        //添加JWT 過濾器filter
        httpSecurity.addFilterBefore(this.jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //添加自定義未授權和未登錄結果返回
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

    /**
     * 在用戶名和密碼校驗前添加的過濾器,如果有token信息,會自行根據token信息進行登錄
     * @return
     * @throws Exception
     */
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() throws Exception{
        return new JwtAuthenticationTokenFilter();
    }

    /**
     * 配置UserDetailsService 和 PasswordEncoder
     * @param auth
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    /**
     * SpringSecurty編碼比對密碼
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        //獲取登錄用戶信息
        return username->{
            UmsAdmin umsAdmin = umsAdminService.getAdminByUsername(username);
            Assert.notNull(umsAdmin,"用戶名不存在!");
            List<UmsPermission> permissionList = umsAdminService.getPermissionList(umsAdmin.getId());
            return new AdminUserDetails(umsAdmin,permissionList);
        };
    }

}

七、登錄與注冊功能的實現

  1、在controller包下新建UmsAdminController類

package com.zzb.test.admin.controller;

import cn.hutool.core.lang.Assert;
import com.zzb.test.admin.common.CommonResult;
import com.zzb.test.admin.mbg.model.UmsAdmin;
import com.zzb.test.admin.mbg.model.UmsPermission;
import com.zzb.test.admin.service.UmsAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 用戶管理Controller
 * Created by zzb on 2019/11/22 9:37
 */
@Api(tags = "UmsAdminController", description = "用戶管理")
@Controller
public class UmsAdminController {
    @Autowired
    private UmsAdminService umsAdminService;
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    /**
     * 用戶注冊
     * @param umsAdminParam
     * @return
     */
    @ApiOperation("用戶注冊")
    @ResponseBody
    @RequestMapping(value = "/admin/register", method = RequestMethod.POST)
    public CommonResult register(@RequestBody UmsAdmin umsAdminParam){
        String originPassword = umsAdminParam.getPassword();
        //注冊用戶賬號
        UmsAdmin umsAdmin = umsAdminService.register(umsAdminParam);
        Assert.notNull(umsAdmin,"用戶注冊失敗");
        //注冊成功后自動登錄
        String token = umsAdminService.login(umsAdminParam.getUsername(),originPassword);
        if (StringUtils.isEmpty(token)) {
            return CommonResult.failed("自動登錄失敗!!");
        }
        Map<String,String> tokenMap = new HashMap<>();
        tokenMap.put("token",token);
        tokenMap.put("tokenHead", tokenHead);

        return CommonResult.success(tokenMap,"自動登錄成功!!");
    }

    /**
     * 用戶登錄
     * @param umsAdminParam
     * @return
     */
    @ApiOperation("用戶登錄")
    @ResponseBody
    @RequestMapping(value = "/admin/login", method = RequestMethod.POST)
    public CommonResult login(@RequestBody UmsAdmin umsAdminParam){
        //根據輸入的用戶名和密碼生成token
        String token = umsAdminService.login(umsAdminParam.getUsername(),umsAdminParam.getPassword());
        if (StringUtils.isEmpty(token)) {
            return CommonResult.failed("用戶名或密碼錯誤!!");
        }
        Map<String,String> tokenMap = new HashMap<>();
        tokenMap.put("token",token);
        tokenMap.put("tokenHead", tokenHead);

        return CommonResult.success(tokenMap);
    }

    /**
     * 獲取用戶所有的權限
     * @param adminId
     * @return
     */
    @ApiOperation("獲取用戶所有權限")
    @ResponseBody
    @RequestMapping(value = "/admin/getPermissionList/{adminId}", method = RequestMethod.POST)
    public CommonResult<List<UmsPermission>> getPermissionList(@PathVariable Long adminId){
        List<UmsPermission> permissionList = umsAdminService.getPermissionList(adminId);
        return CommonResult.success(permissionList);
    }


}

  2、修改SwaggerConfig類,通過修改swagger-ui配置實現調用接口自帶Authorization頭,這樣就可以訪問需要登錄的接口了

package com.zzb.test.admin.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;
import java.util.List;

/**
 * Swagger-UI的配置類
 * Created by zzb on 2019/11/18 18:14
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createApi(){
        return new Docket(DocumentationType.SWAGGER_2)
                //文檔head主體
                .apiInfo(this.apiInfo())
                //生成Controller的api文檔
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.zzb.test.admin.controller"))
                //為有@Api注解的Controller生成API文檔
//                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                //為有@ApiOperation注解的方法生成API文檔
//                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build()
                //添加登錄認證
                .securitySchemes(this.securitySchemes())
                .securityContexts(this.securityContexts());
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("Swagger-UI演示")
                .description("shop-test")
                .version("1.0.0")
                .build();
    }

    private List<ApiKey> securitySchemes(){
        //設置請求頭信息
        List<ApiKey> result = new ArrayList<>();
        ApiKey apiKey = new ApiKey("Authorization","Authorization","header");
        result.add(apiKey);
        return result;
    }

    private List<SecurityContext> securityContexts(){
        //設置需要登錄認證的路徑
        List<SecurityContext> result = new ArrayList<>();
        result.add(this.getContextByPath("/admin/brand/.*"));
        return result;
    }

    private SecurityContext getContextByPath(String pathRegex){
        return SecurityContext.builder()
                .securityReferences(this.defaultAuth())
                .forPaths(PathSelectors.regex(pathRegex))
                .build();
    }

    private List<SecurityReference> defaultAuth(){
        List<SecurityReference> result = new ArrayList<>();
        AuthorizationScope authorizationScope = new AuthorizationScope("global","全部通過");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        result.add(new SecurityReference("Authorization", authorizationScopes));
        return result;
    }
}

  3、給PmsBrandController接口中的方法添加訪問權限

  · 給查詢接口添加pms:brand:read權限

@ApiOperation("分頁獲取所有品牌")
    @RequestMapping(value = "/admin/brand/getList", method = RequestMethod.GET)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:brand:read')")
    public CommonResult<CommonPage<PmsBrand>> getList(@RequestParam(value = "pageNum", defaultValue = "1") @ApiParam("分頁頁碼") Integer pageNum,
                                                           @RequestParam(value = "pageSize", defaultValue = "10") @ApiParam("每頁數量") Integer pageSize){
        List<PmsBrand> list = pmsBrandService.getList(pageNum,pageSize);
        logger.info("分頁查詢所有品牌==》" + list);
        return CommonResult.success(CommonPage.restPage(list));
    }

  · 給修改接口添加pms:brand:update權限

@ApiOperation("添加品牌")
    @RequestMapping(value = "/admin/brand/insert", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:brand:create')")
    public CommonResult insert(@ApiParam("品牌信息") PmsBrand pmsBrand){
        int count = pmsBrandService.insert(pmsBrand);
        logger.info("添加品牌==》" + count);
        if (count>0) {
            return CommonResult.success("添加品牌成功");
        }
        return CommonResult.failed();
    }

  · 給刪除接口添加pms:brand:delete權限

@ApiOperation("刪除品牌")
    @RequestMapping(value = "/admin/brand/delete", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:brand:delete')")
    public CommonResult delete(@ApiParam("品牌id") Long id){
        int count = pmsBrandService.delete(id);
        logger.info("刪除品牌==》" + count);
        if (count>0) {
            return CommonResult.success("刪除品牌成功");
        }
        return CommonResult.failed();
    }

  · 給添加接口添加pms:brand:create權限

@ApiOperation("更新品牌")
    @RequestMapping(value = "/admin/brand/update", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:brand:update')")
    public CommonResult update(@ApiParam("修改主體") PmsBrand pmsBrand){
        int count = pmsBrandService.update(pmsBrand);
        logger.info("更新品牌==》" + count);
        if (count>0) {
            return CommonResult.success("更新品牌成功");
        }
        return CommonResult.failed();
    }

八、測試與驗證

  1、未登錄直接訪問/admin/brand/getList接口,分頁獲取所有品牌

  

   

  2、注冊新用戶,訪問/admin/register接口

  

   

  查看數據庫表ums_admin,有test用戶存在,id為8

   3、未賦予權限,登錄后直接訪問品牌/admin/brand/getList接口,分頁獲取所有品牌

  swagger登錄設置token

  

   訪問接口

  

 

   

   4、給test用戶賦予相應的訪問權限

  配置權限表,向ums_permission表添加5條數據

   給id為8的test用戶設置讀的權限

  5、賦予權限后再次訪問接口/admin/brand/getList

  

 

  項目github地址:https://github.com/18372561381/shoptest


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM