spring boot 2 + shiro 實現權限管理

Shiro是一個功能強大且易於使用的Java安全框架,主要功能有身份驗證、受權、加密和會話管理。
看了網上一些文章,下面2篇文章寫得不錯。
Springboot2.0 集成shiro權限管理 
Spring Boot:整合Shiro權限框架 html

本身動手敲了下代碼,在第一篇文章上加入了第二篇文章的Swagger測試,另外本身加入lombok簡化實體類代碼,一些地方代碼也稍微修改了下,過程當中也碰到一些問題,最終代碼成功運行。java

開發版本:
IntelliJ IDEA 2019.2.2
jdk1.8
Spring Boot 2.1.11
MySQL8.0mysql

1、建立SpringBoot項目,添加依賴包和配置application.ymlweb

在IDEA中建立一個新的SpringBoot項目算法

一、pom.xml引用的依賴包以下:spring

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

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

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

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>

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

二、application.ymlsql

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update #指定爲update,每次啓動項目檢測表結構有變化的時候會新增字段,表不存在時會新建,若是指定create,則每次啓動項目都會清空數據並刪除表,再新建
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl #按字段名字建表
        #implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl #駝峯自動映射爲下劃線格式
    show-sql: true # 默認false,在日誌裏顯示執行的sql語句
    database: mysql
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

2、建立實體類數據庫

建立User、Role、Permission三個實體類,根據規則會自動生成兩個中間表,最終數據庫有5個表。
另外添加一個model處理登陸結果。apache

一、Userapi

package com.example.shiro.entity; import lombok.Getter; import lombok.Setter; import org.springframework.format.annotation.DateTimeFormat; import javax.persistence.*; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @Entity @Getter @Setter public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long userId; @Column(nullable = false, unique = true) private String userName; //登陸用戶名
 @Column(nullable = false) private String name;//名稱(暱稱或者真實姓名,根據實際狀況定義)
 @Column(nullable = false) private String password; private String salt;//加密密碼的鹽

    private byte state;//用戶狀態,0:建立未認證(好比沒有激活,沒有輸入驗證碼等等)--等待驗證的用戶 , 1:正常狀態,2:用戶被鎖定.
 @ManyToMany(fetch= FetchType.EAGER)//當即從數據庫中進行加載數據;
    @JoinTable(name = "UserRole", joinColumns = { @JoinColumn(name = "userId") }, inverseJoinColumns ={@JoinColumn(name = "roleId") }) private List<Role> roleList;// 一個用戶具備多個角色
 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime createTime;//建立時間
 @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate expiredDate;//過時日期

    private String email; /**密碼鹽. 從新對鹽從新進行了定義,用戶名+salt,這樣就不容易被破解 */
    public String getCredentialsSalt(){ return this.userName+this.salt; } }

說明:
這裏使用@Getter,@Setter註解,不能使用@Data註解,由於實體使用了jpa的@oneToMany ,加載方式爲lazy,在主表查詢時關聯表未加載,而主表使用@Data後會實現帶關聯表屬性的hashCode和equals等方法。在運行過程當中調用關聯表數據時會顯示異常 java.lang.stackoverflowerror。

二、Role

package com.example.shiro.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.List; @Entity @Getter @Setter public class Role { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long roleId; // 編號
 @Column(nullable = false, unique = true) private String role; // 角色標識程序中判斷使用,如"admin",這個是惟一的:

    private String description; // 角色描述,UI界面顯示使用

    private Boolean available = Boolean.TRUE; // 是否可用,若是不可用將不會添加給用戶 //角色 -- 權限關係:多對多關係;
    @ManyToMany(fetch= FetchType.EAGER) @JoinTable(name="RolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")}) private List<Permission> permissions; // 用戶 - 角色關係定義;
 @ManyToMany @JoinTable(name="UserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="userId")}) private List<User> users;// 一個角色對應多個用戶
}

三、Permission

package com.example.shiro.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.List; @Entity @Getter @Setter public class Permission { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long permissionId;//主鍵.
 @Column(nullable = false) private String permissionName;//名稱.
 @Column(columnDefinition="enum('menu','button')") private String resourceType;//資源類型,[menu|button]

    private String url;//資源路徑.

    private String permission; //權限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view

    private Long parentId; //父編號

    private String parentIds; //父編號列表

    private Boolean available = Boolean.TRUE; //角色 -- 權限關係:多對多關係;
    @ManyToMany(fetch= FetchType.EAGER) @JoinTable(name="RolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")}) private List<Role> roles; }

四、LoginResult

package com.example.shiro.model; import lombok.Data; @Data public class LoginResult { private boolean isLogin = false; private String result; }

3、DAO

一、添加一個DAO基礎接口:BaseRepository

package com.example.shiro.repository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; import java.io.Serializable; @NoRepositoryBean public interface BaseRepository<T, I extends Serializable> extends PagingAndSortingRepository<T, I>, JpaSpecificationExecutor<T> { }

二、UserRepository

package com.example.shiro.repository; import com.example.shiro.entity.User; public interface UserRepository extends BaseRepository<User,Long> { User findByUserName(String userName); }

4、Service

一、LoginService

package com.example.shiro.service; import com.example.shiro.model.LoginResult; public interface LoginService { LoginResult login(String userName, String password); void logout(); }

二、UserService

package com.example.shiro.service; import com.example.shiro.entity.User; public interface UserService { User findByUserName(String userName); }

5、Service.impl

一、LoginServiceImpl

package com.example.shiro.service.impl; import com.example.shiro.model.LoginResult; import com.example.shiro.repository.UserRepository; import com.example.shiro.service.LoginService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Service; @Service public class LoginServiceImpl implements LoginService { @Override public LoginResult login(String userName, String password) { LoginResult loginResult = new LoginResult(); if (userName == null || userName.isEmpty()) { loginResult.setLogin(false); loginResult.setResult("用戶名爲空"); return loginResult; } String msg = ""; // 一、獲取Subject實例對象
        Subject currentUser = SecurityUtils.getSubject(); //        // 二、判斷當前用戶是否登陸 // if (currentUser.isAuthenticated() == false) { //
// } // 三、將用戶名和密碼封裝到UsernamePasswordToken
        UsernamePasswordToken token = new UsernamePasswordToken(userName, password); // 四、認證
        try { currentUser.login(token);// 傳到MyAuthorizingRealm類中的方法進行認證
            Session session = currentUser.getSession(); session.setAttribute("userName", userName); loginResult.setLogin(true); return loginResult; //return "/index";
        } catch (UnknownAccountException e) { e.printStackTrace(); msg = "UnknownAccountException -- > 帳號不存在:"; } catch (IncorrectCredentialsException e) { msg = "IncorrectCredentialsException -- > 密碼不正確:"; } catch (AuthenticationException e) { e.printStackTrace(); msg = "用戶驗證失敗"; } loginResult.setLogin(false); loginResult.setResult(msg); return loginResult; } @Override public void logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); } }

二、UserServiceImpl

package com.example.shiro.service.impl; import com.example.shiro.entity.User; import com.example.shiro.repository.UserRepository; import com.example.shiro.service.UserService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class UserServiceImpl implements UserService { @Resource private UserRepository userRepository; @Override public User findByUserName(String userName) { return userRepository.findByUserName(userName); } }

6、config配置類

一、建立Realm

package com.example.shiro.config; import com.example.shiro.entity.Permission; import com.example.shiro.entity.Role; import com.example.shiro.entity.User; import com.example.shiro.service.UserService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import javax.annotation.Resource; public class MyShiroRealm extends AuthorizingRealm { @Resource private UserService userService; /** * 身份認證:驗證用戶輸入的帳號和密碼是否正確。 * */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //獲取用戶輸入的帳號
        String userName = (String) token.getPrincipal(); //經過username從數據庫中查找 User對象. //實際項目中,這裏能夠根據實際狀況作緩存,若是不作,Shiro本身也是有時間間隔機制,2分鐘內不會重複執行該方法
        User user = userService.findByUserName(userName); if (user == null) { return null; } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user,//這裏傳入的是user對象,比對的是用戶名,直接傳入用戶名也沒錯,可是在受權部分就須要本身從新從數據庫裏取權限
                user.getPassword(),//密碼
                ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt
                getName()//realm name
 ); return authenticationInfo; } /** * 權限信息 * */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //若是身份認證的時候沒有傳入User對象,這裏只能取到userName //也就是SimpleAuthenticationInfo構造的時候第一個參數傳遞須要User對象
        User user  = (User)principals.getPrimaryPrincipal(); for(Role role : user.getRoleList()){ //添加角色
 authorizationInfo.addRole(role.getRole()); for(Permission p:role.getPermissions()){ //添加權限
 authorizationInfo.addStringPermission(p.getPermission()); } } return authorizationInfo; } }

二、配置Shiro

package com.example.shiro.config; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import java.util.HashMap; import java.util.Map; import java.util.Properties; @Configuration public class ShiroConfig { //將本身的驗證方式加入容器
 @Bean MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } //權限管理,配置主要是Realm的管理認證
 @Bean DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myShiroRealm()); return manager; } //憑證匹配器(密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理)
 @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這裏使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次數,好比散列兩次,至關於 md5(md5(""));
        return hashedCredentialsMatcher; } // Filter工廠,設置對應的過濾條件和跳轉條件
 @Bean ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); Map<String, String> filterMap = new HashMap<String, String>(); // 登出
        filterMap.put("/logout", "logout"); // swagger
        filterMap.put("/swagger**/**", "anon"); filterMap.put("/webjars/**", "anon"); filterMap.put("/v2/**", "anon"); // 對全部用戶認證
        filterMap.put("/**", "authc"); // 登陸
        bean.setLoginUrl("/login"); // 首頁
        bean.setSuccessUrl("/index"); // 未受權頁面,認證不經過跳轉
        bean.setUnauthorizedUrl("/403"); bean.setFilterChainDefinitionMap(filterMap); return bean; } //開啓shiro aop註解支持.
 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; } //shiro註解模式下,登陸失敗或者是沒有權限都是拋出異常,而且默認的沒有對異常作處理,配置一個異常處理
    @Bean(name="simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError");//數據庫異常處理
        mappings.setProperty("UnauthorizedException","/403"); r.setExceptionMappings(mappings); // None by default
        r.setDefaultErrorView("error");    // No default
        r.setExceptionAttribute("exception");     // Default is "exception"
        return r; } }

三、配置swagger

package com.example.shiro.config; import io.swagger.annotations.ApiOperation; 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.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()).build(); } private static ApiInfo apiInfo() { return new ApiInfoBuilder() .title("API文檔") .description("Swagger API 文檔") .version("1.0") .contact(new Contact("name..", "url..", "email..")) .build(); } }

7、controller

一、LoginController用來處理登陸

package com.example.shiro.controller; import com.example.shiro.entity.User; import com.example.shiro.model.LoginResult; import com.example.shiro.service.LoginService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class LoginController { @Resource private LoginService loginService; @GetMapping(value = "/login") public String login() { return "登陸頁"; } @PostMapping(value = "/login") public String login(@RequestBody User user) { System.out.println("login()"); String userName = user.getUserName(); String password = user.getPassword(); LoginResult loginResult = loginService.login(userName,password); if(loginResult.isLogin()){ return "登陸成功"; } else { return "登陸失敗:" + loginResult.getResult(); } } @GetMapping(value = "/index") public String index() { return "主頁"; } @GetMapping(value = "/logout") public String logout() { return "退出"; } @GetMapping("/403") public String unauthorizedRole(){ return "沒有權限"; } }

二、UserController用來測試訪問,權限所有采用註解的方式。

package com.example.shiro.controller; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { //用戶查詢
    @GetMapping("/userList") @RequiresPermissions("user:view")//權限管理;
    public String userInfo(){ return "userList"; } //用戶添加
    @GetMapping("/userAdd") @RequiresPermissions("user:add")//權限管理;
    public String userInfoAdd(){ return "userAdd"; } //用戶刪除
    @GetMapping("/userDel") @RequiresPermissions("user:del")//權限管理;
    public String userDel(){ return "userDel"; } }

8、數據庫預設一些數據

先運行一遍程序,JPA生成數據庫表後,手工執行sql腳本插入樣本數據。
用戶admin的原始密碼是123456。

INSERT INTO `user` (`userId`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理員', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 1); INSERT INTO `permission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (1,1,'用戶管理',0,'0/','user:view','menu','user/userList'); INSERT INTO `permission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (2,1,'用戶添加',1,'0/1','user:add','button','user/userAdd'); INSERT INTO `permission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (3,1,'用戶刪除',1,'0/1','user:del','button','user/userDel'); INSERT INTO `role` (`roleid`,`available`,`description`,`role`) VALUES (1,1,'管理員','admin'); INSERT INTO `rolepermission` (`permissionid`,`roleid`) VALUES (1,1); INSERT INTO `rolepermission` (`permissionid`,`roleid`) VALUES (2,1); INSERT INTO `userrole` (`roleid`,`userId`) VALUES (1,1);

9、swagger測試

 一、啓動項目,訪問http://localhost:8080/swagger-ui.html

  二、訪問/user/userAdd, Response body顯示登陸頁

 三、訪問POST的/login,請求參數輸入:

{
"userName": "admin",
"password": "123456"
}

 Response body顯示登陸成功。

 四、再次訪問/user/userAdd,由於登陸成功了而且有權限,此次Response body顯示userAdd

 

 五、訪問/user/userDel,由於數據庫沒有配置權限,因此Response body顯示沒有權限

相關文章
相關標籤/搜索