shiro授權和認證(四)

只陪你發表於2020-10-28

SpringBoot+shiro+Swagger實現前後分離的框架

1、導包

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.54</version>
        </dependency>


        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>



        <!--下面匯入資料庫的使用的包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>


        <!--使用Druid這個連線池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.0</version>
        </dependency>



        <!--下面要匯入Swagger2的相關的包-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>

2、封裝token

public class CustomToken extends UsernamePasswordToken {

    private String token;   //使用者身份唯一的標識
    //這個token是在認證通過之後  使用者訪問其他資源的時候 來進行你給身份識別的

    public CustomToken(String token){
       this.token=token;
    }

    @Override
    public Object getPrincipal() {
        //在使用者認證通過之後 再訪問這個方法 預設返回的是什麼?
        // Realm校驗的第一個引數
        //校驗我們自己寫了   還有沒有第一個引數這個說法?
        return token;
    }
}

3、編寫過濾器

public class CustomAccessControllerFilter extends AccessControlFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request= (HttpServletRequest) servletRequest;
        try {
            //校驗身份
            //邏輯是什麼?
            //第一步:獲取token
            String token=request.getHeader(Constant.REQ_TOKEN);
            //第二步:看下這個token是否否為""
            if(StringUtils.isEmpty(token)){  //說明你娃身份是非法的
               throw new BusinessException(400001,"使用者的請求的token不能為空");
            }else{  //說明使用者帶了token
                //邏輯
                //這裡要將token進行封裝  封裝完了 交給shiro去做認證  看下身份是否合法
                CustomToken customToken = new CustomToken(token);
                //記住下面這個類 在使用者第一次登陸的時候  並不會執行
                // 這個執行的時候 是在認證成功之後訪問其他資源的
                //的時候 機械能給你身份校驗的
                getSubject(servletRequest,servletResponse).login(customToken);
            }
        } catch (BusinessException e) {
           //如果是這個異常:返回JSON告訴前端出現問題了
           resultResponse(e.getMessageCode(),e.getDefaultMesaage(),servletResponse);
           return false;
        } catch (AuthenticationException e) {  //校驗沒通過的異常
            //  e.getCause() :返回的是當前異常的例項
            if(e.getCause() instanceof BusinessException){ //表示返回的是我們自定義的異常
                //將異常的例項進行轉換
                BusinessException err= (BusinessException) e.getCause();
                resultResponse(err.getMessageCode(),err.getDefaultMesaage(),servletResponse);
            }else{  //如果執行到這裡  說明 這個異常是shiro返回的
                resultResponse(400001,"使用者的認證是失敗的",servletResponse);
            }
            return false;
        }catch (AuthorizationException e){
            //  e.getCause() :返回的是當前異常的例項
            if(e.getCause() instanceof BusinessException){ //表示返回的是我們自定義的異常
                //將異常的例項進行轉換
                BusinessException err= (BusinessException) e.getCause();
                resultResponse(err.getMessageCode(),err.getDefaultMesaage(),servletResponse);
            }else{  //如果執行到這裡  說明 這個異常是shiro返回的
                resultResponse(403001,"使用者沒有訪問許可權",servletResponse);
            }
            return false;
        }catch (Exception e){  //這個分支就捕獲一些未考慮的異常了
            //  e.getCause() :返回的是當前異常的例項
            if(e.getCause() instanceof BusinessException){ //表示返回的是我們自定義的異常
                //將異常的例項進行轉換
                BusinessException err= (BusinessException) e.getCause();
                resultResponse(err.getMessageCode(),err.getDefaultMesaage(),servletResponse);
            }else{  //如果執行到這裡  說明 這個異常是shiro返回的
                resultResponse(500001,"系統出現了異常",servletResponse);
            }
            return false;
        }
        //當前的方法返回true才放行  否則這個程式也就執行到這裡了....
        return true;
    }

    /**
     * 這個方法的主要功能就是告訴前端 一些出錯的資訊
     * @param messageCode
     * @param defaultMesaage
     * @param response
     */
    private void resultResponse(int messageCode, String defaultMesaage, ServletResponse response) {

        //構建返回的資料
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code",messageCode);
        jsonObject.put("msg",defaultMesaage);
        //設定下返回的資料型別
        response.setContentType(MediaType.APPLICATION_JSON_UTF8.toString());
        //獲取輸出流
        try {
            ServletOutputStream out = response.getOutputStream();
            //接下來項資料寫出去
            out.write(jsonObject.toJSONString().getBytes());
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4、編寫憑證匹配器

public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {

    @Autowired
    private IUserService userService;

    /**
     * 下面這個方法 返回true 或者 false就決定了 這個是成功呢還是失敗
     * @param token
     * @param info
     * @return
     */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        //  這裡面要實現的功能很簡單
        //  把前面傳遞過來的token取出來  再把儲存到伺服器的token取出來做比較
        // 如果一致那麼就返回true  否則就返回  false
        CustomToken token1= (CustomToken) token;
        // 取出 Principal的值 (下面這個值 就是從前端傳遞過來進行比較的)
        String tokenVal= (String) token1.getPrincipal();
        // 從redis  或者  資料庫    或者 session取出這個資訊來
        //假設取出來了....
        //String tokenServer="xiaoboboxiaowangzi";
        boolean b=false;
        try{
            b = userService.tokenExistsOrNot(tokenVal);
        }catch (Exception err){
            throw new BusinessException(500001,"查詢token存在失敗"+err.getMessage());
        }
        //進行比較 判定前端的token和服務端的token是否一致  如果一致  那麼返回true  否則返回false
        if(!b){
            throw new BusinessException(4010000,"授權資訊無效請重新登入");
        }
        return true;
    }
}

5、編寫realm

public class CustomRealm extends AuthorizingRealm {
    /**
     * 授權的方法
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        return simpleAuthorizationInfo;
    }

    /**
     * 認證的方法
     * 將前端放進去的token 取出來 放到校驗的SimpleAuthenticationInfo中去 方便後面進行校驗
     * token放到哪裡呢?
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            //取出前端傳遞過來的token
            CustomToken customToken= (CustomToken) authenticationToken;
            String token= (String) customToken.getPrincipal();

        //這裡就可以取出這個token
        //在這裡要將前端傳遞過來的token給封裝到 SimpleAuthenticationInfo 物件中去
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token,token,getName());
        return simpleAuthenticationInfo;
    }
}

6、編寫Swagger的配置檔案

@SpringBootConfiguration
@EnableSwagger2
public class SwaggerConfig {


    @Bean
    public Docket createApi(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.test.shiro.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    /**
     * 頁面資訊的配置
     * @return
     */
    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("springboot+shiro+swagger測試")
                .description("這裡是整合shiro和Swagger實現前後分離")
                .version("v1.0")
                .build();
    }
}

7、編寫shiro的配置檔案

@SpringBootConfiguration
public class ShiroConfig {

    /**
     * 咋們專案認證(請求資源的時候 身份的認證)的realm
     * @return
     */
    @Bean
    public CustomRealm customRealm(){
        CustomRealm customRealm = new CustomRealm();
        customRealm.setCredentialsMatcher(customHashedCredentialsMatcher());
        return customRealm;
    }

    /**
     * 憑證匹配器的申明
     * @return
     */
    @Bean
    public CustomHashedCredentialsMatcher customHashedCredentialsMatcher(){
       return new CustomHashedCredentialsMatcher();
    }


    /**
     * 安全管理器
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm());
        return securityManager;
    }

    /**
     * 配置的是目標過濾器的代理
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean  shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //配置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //接下來配置些過濾器
        //配置自己的那個過濾器
        Map<String, Filter> maps=new LinkedHashMap<>();
        maps.put("token",new CustomAccessControllerFilter());
        shiroFilterFactoryBean.setFilters(maps);

        //對請求過濾和攔截的設定
        Map<String,String> maps1=new LinkedHashMap<>();
        //放入不攔截的頁面  攔截的頁面....
        maps1.put("/user/login","anon");
        //Swagger的所有請求的資源和請求的地址都不需要攔截
        maps1.put("/swagger/**","anon");
        maps1.put("/v2/api-docs","anon");
        maps1.put("/swagger-ui.html","anon");
        maps1.put("/swagger-resources/**","anon");
        maps1.put("/webjars/**","anon");
        maps1.put("/favicon.ico","anon");
        maps1.put("/captcha.jpg","anon");
        maps1.put("/csrf","anon");
        //設定我們自己的校驗
        maps1.put("/**","token,authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(maps1);
        return shiroFilterFactoryBean;
    }

    /**
     * 開啟aop的註解的支援
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultSecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        attributeSourceAdvisor.setSecurityManager(securityManager);
        return attributeSourceAdvisor;
    }
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
}

8、編寫專案的配置檔案

@SpringBootConfiguration
@ComponentScan(basePackages = {"com.qf.shiro"})
@MapperScan(basePackages = {"com.qf.shiro.mapper"})
public class AppConfig {

}

9、結果集的封裝

9.1、介面的封裝

public interface ResponseCodeInterface {
    /**
     * 返回的碼的一個獲取
     * @return
     */
    int getCode();

    /**
     * 返回訊息的獲取
     * @return
     */
    String getMsg();
}

9.2、返回資訊碼值和提示資訊的封裝

public enum BaseResponseCode implements ResponseCodeInterface{
    /**
     * 接下來就要和前端約束好所有的碼值以及含義
     */
    SUCCESS(0,"操作成功"),
    SYSTEM_ERROR(5000001,"系統錯誤"),
    METHOD_INVALIDATE(4000001,"資料校驗出錯"),
    DATA_ERROR(4000002,"傳入資料異常"),
    TOKEN_NOT_NULL(4010001,"使用者認證異常");

    //整個建構函式
    BaseResponseCode(int code,String msg){
        this.code=code;
        this.msg=msg;
    }

    private int code;
    private String msg;

    @Override
    public int getCode() {
        return code;
    }

    @Override
    public String getMsg() {
        return msg;
    }
}

9.3、返回資料的封裝

@Data
public class DataResult <T> {
    private int code;   //返回的碼值
    private String msg; //返回的錯誤資訊提示
    private T data;   //返回的資料

    //下面這一塊是對構造器進行封裝

    public DataResult(int code,T data){
       this.code=code;
       this.data=data;
       this.msg=null;
    }

    public DataResult(int code,String msg,T data){
          this.code=code;
          this.data=data;
          this.msg=msg;
    }

    public DataResult(int code,String msg){
        this.code=code;
        this.msg=msg;
    }

    public DataResult(){
        this.code=BaseResponseCode.SUCCESS.getCode();
        this.msg=BaseResponseCode.SUCCESS.getMsg();
        this.data=null;
    }

    public DataResult(T data){
        this.code=BaseResponseCode.SUCCESS.getCode();
        this.msg=BaseResponseCode.SUCCESS.getMsg();
        this.data=data;
    }

    public DataResult(ResponseCodeInterface responseCodeInterface){
         this.data=null;
         this.code=responseCodeInterface.getCode();
         this.msg=responseCodeInterface.getMsg();
    }

    public DataResult(ResponseCodeInterface responseCodeInterface,T data){
         this.data=data;
         this.code=responseCodeInterface.getCode();
         this.msg=responseCodeInterface.getMsg();
    }

    /**
     * 這個很牛逼
     * 不帶資料的返回
     * @param <T>
     * @return
     */
    public static <T>DataResult success(){
       return new DataResult();
    }

    /**
     * 帶資料的返回
     * @param data
     * @param <T>
     * @return
     */
    public static <T>DataResult success(T data){
       return new DataResult(data);
    }

    /**
     * 自己給引數的問題
     * @param code
     * @param msg
     * @param data
     * @param <T>
     * @return
     */
    public static <T>DataResult getResult(int code,String msg,T data){
        return new <T>DataResult(code,msg,data);
    }
    /**
     * 自己給引數的問題
     * @param code
     * @param msg
     * @param <T>
     * @return
     */
    public static <T>DataResult getResult(int code,String msg){
        return new <T>DataResult(code,msg);
    }

    /**
     * 直接傳遞一個列舉進來
     * @param baseResponseCode
     * @param <T>
     * @return
     */
    public static <T>DataResult getResult(BaseResponseCode baseResponseCode){
        return new <T>DataResult(baseResponseCode);
    }


    /**
     * 直接傳遞一個列舉進來
     * @param baseResponseCode
     * @param <T>
     * @return
     */
    public static <T>DataResult getResult(BaseResponseCode baseResponseCode,T data){
        return new <T>DataResult(baseResponseCode,data);
    }
}

10、controller的編寫

@RestController
@Api(tags = {"使用者介面"})
public class UserController {

    @Autowired
    private IUserService userService;

    private Logger logger= LoggerFactory.getLogger(UserController.class);

    /**
     * 登陸的介面
     * @param user
     * @return
     */
    @RequestMapping(value = "/user/login",method = RequestMethod.POST)
    @ApiOperation(value = "使用者登陸的介面")
    public DataResult<User> login(@RequestBody User user){
       //這個裡面應該幹什麼?
        /**
         * 說白了 呼叫業務邏輯層的方法
         *  異常的捕獲
         *  返回資料
         */
        DataResult<User> dataResult=null;
        try {
            User user1 = userService.login(user);
            dataResult=DataResult.success(user1);
        } catch (Exception e) {
            if(e instanceof BusinessException){  //說明是業務異常
                BusinessException err= (BusinessException) e;
                //應該幹什麼?
                dataResult=new DataResult<>(err.getMessageCode(),err.getDefaultMesaage());
            }else{
                //dataResult=new DataResult<>(500001,"系統異常造成登陸失敗");
                dataResult=DataResult.getResult(BaseResponseCode.SYSTEM_ERROR.getCode(),BaseResponseCode.SYSTEM_ERROR.getMsg());
            }
            return dataResult;
        }
        return dataResult;
    }

    /**
     * 查詢所有的使用者資料
     * @return
     */
    @RequestMapping(value = "/user/list",method = RequestMethod.GET)
    @ApiOperation(value = "獲取所有的使用者資訊")
    @ApiImplicitParam(paramType = "header",name = "token",value = "使用者token",required = true,dataType = "String")
   public DataResult<List<User>> findUserList(){
       //定義返回資料
       DataResult<List<User>> userLists;
       try{
           //返回使用者資料
           List<User> users = userService.findUserList();
           userLists=DataResult.success(users);
           logger.info("獲取資料成功....");
       }catch (Exception err){
           //說明獲取資訊失敗了
           logger.error("獲取使用者資訊失敗:"+err.fillInStackTrace());
           userLists=DataResult.getResult(BaseResponseCode.SYSTEM_ERROR.getCode(),BaseResponseCode.SYSTEM_ERROR.getMsg());
       }
       return userLists;
   }
}

11、Service的編寫

11.1、Service介面的編寫

public interface IUserService {
    /**
     * 登陸
     * @param user
     * @return
     */
       User login(User user);

    /**
     * 通過名字找到使用者
     * @param userName
     * @return
     */
       User findUserByName(String userName);

     /**
       * 更新token到資料庫
      * @param user
      */
      void updateToken(User user);

    /**
     * 查詢所有的使用者
     * @return
     */
     List<User> findUserList()throws Exception;

    /**
     * 判定這個token是否存在
     * @param token
     * @return
     */
     boolean tokenExistsOrNot(String token);

}

11.2、Service實現的編寫

@Service
@Transactional
public class UserService implements IUserService {


    @Autowired
    private UserMapper userMapper;


    @Override
    public User login(User user){
        //這個類裡面應該幹什麼?
        /**第一步:獲取到前端傳遞過來的使用者名稱
         *第二步:通過使用者名稱 獲取使用者物件
         * 第三步:校驗
         * 第四步:生成token儲存到資料庫
         * 第五步:將token封裝到返回資料裡面給前端
         */
        //獲取使用者名稱
        String userName = user.getUserName();
        //通過使用者名稱 找使用者名稱找物件
        User userResult = findUserByName(userName);
        //第三步:校驗
        if(null==userResult){  //說明使用者名稱不對
            throw new BusinessException(40001,"使用者名稱不對");
        }
        //說明:使用者名稱是對的
        //比較密碼
        if(!(userResult.getPassword().equals(user.getPassword()))){
            throw new BusinessException(40002,"密碼不對");
        }
        //執行到這裡說明使用者身份合法的
        //先將資料儲存到一個類裡面
        //首先要生成token這個值
        String token= UUID.randomUUID().toString();
        Date date=new Date();
        //設定這個值給user物件
        userResult.setToken(token);
        userResult.setExpireDate(date);
        //下面就是更新這個資料庫的資料
        updateToken(userResult);
        //將這個資訊返回給前端
        //一般情況下 密碼是不需要返回的
        userResult.setPassword("");
        //設定返回資料的物件
        //DataResult<User> userDataResult = new DataResult<>(0,"認證成功",userResult);
        return userResult;
    }

    @Override
    public User findUserByName(String userName) {
        return userMapper.findUserByName(userName);
    }

    @Override
    public void updateToken(User user) {
        userMapper.updateToken(user);
    }

    @Override
    public List<User> findUserList() throws Exception{
        List<User> userList = userMapper.findUserList();
        //接下來對資料進行封裝
        DataResult<List<User>> dataResult = new DataResult<>(0, "請求完美", userList);
        return userList;
    }

    @Override
    public boolean tokenExistsOrNot(String token) {
        //通過token查詢使用者資訊
        User userResult = userMapper.findUserByToken(token);
        //接下來就要判斷了
        if(null!=userResult){
           return true;
        }
        return false;
    }
}

13、Mapper介面的編寫

public interface UserMapper {

    /**
     * 通過名字找到使用者
     * @param userName
     * @return
     */
    User findUserByName(String userName);

    /**
     * 更新token到資料庫
     * @param user
     */
    void updateToken(User user);

    /**
     * 查詢所有的使用者
     * @return
     */
    List<User> findUserList();
    /**
     * 檢視當前的token是否在資料庫中存在
     * @param token
     * @return
     */
    User findUserByToken(String token);

}

14、mapper.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.qf.shiro.mapper.UserMapper">

    <!--通過使用者名稱找使用者-->
    <select id="findUserByName" parameterType="string" resultType="user">
        select * from t_user where userName=#{value}
    </select>

    <!--更新資料庫使用者的token-->
    <update id="updateToken" parameterType="user">
        update t_user set token=#{token} where id=#{id}
    </update>

    <!--查詢所有的使用者-->
    <select id="findUserList" resultType="user">
        select * from t_user
    </select>
    
    <!--檢視token是否存在-->
    <select id="findUserByToken" parameterType="String" resultType="user">
        select * from t_user where token=#{value}
    </select>
</mapper>

15、使用者物件的編寫

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = -5199670739008077879L;
    private Integer id;
    private String userName;
    private String password;
    private String token;
    private Date expireDate;  //token的過期時間
}

16、常量類的編寫

public class Constant {
    public static final String REQ_TOKEN="token";
}

17、properties檔案的編寫

mybatis.type-aliases-package=com.test.shiro.pojo
mybatis.mapper-locations=classpath:mapper/*.xml

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql:///xxx
spring.datasource.username=root
spring.datasource.password=root

18、自定義異常的編寫

public class BusinessException extends RuntimeException{
    private int messageCode;
    private String defaultMesaage;
    public BusinessException(int messageCode,String defaultMesaage){
          super(defaultMesaage);
          this.messageCode=messageCode;
          this.defaultMesaage=defaultMesaage;
    }

    public String getDefaultMesaage() {
        return defaultMesaage;
    }

    public int getMessageCode() {
        return messageCode;
    }
}

相關文章