Spring Boot2 (XII): hand touch hand to teach you to build Shiro security framework

I. Introduction

SpringBoot + Shiro + Mybatis completed.

Before Shiro looked a little tutorial partner, followed suit, I met find many pit ('இ dish இ `)

Modify the order a bit, successfully ran up. You can be tested by postman

Small Bibi ∠ (ᐛ "∠) _, directly on the source code: https: //github.com/niaobulashi/spring-boot-learning/tree/master/spring-boot-20-shiro

Two, Shiro is valid

Apache Shiro is a powerful, flexible, open source security framework. You can cleanly handle authentication, authorization, session management and enterprise encryption.

Two, Shiro can be doing

  • Authenticate users
  • User access control, such as: 1. If the user is assigned a certain security role. 2, to determine whether the user is granted permission to complete an operation
  • Session API can be arbitrarily used in an environment of non-Web or EJB containers
  • Response authentication, access control, or events Session lifecycle
  • One or more user data source security data may be combined into a composite user "view" (view)
  • Supports single sign-on (SSO) function
  • Support the provision of "Remember Me" service, access to user-related information without having to log on

FIG Shiro frame as follows:

  • Authentication (authentication): user identification, commonly referred to as user "login"
  • Authorization (authorization): access control. For example, if a user has permission to use an operation.
  • Session Management (session management): user-specific session management, even in a non-EJB or web application.
  • Cryptography (encryption): While using an encryption algorithm to encrypt the data source, to ensure ease of use.

In the conceptual level, Shiro architecture consists of three main ideas: Subject, SecurityManager and Realm. The following diagram shows how these components interact, we will in turn be described below.

img

  • Subject: the current user, Subject can be one person, but may also be third-party service, the daemon accounts, cron job or other - any current events and software interaction.
  • SecurityManager: manage all Subject, SecurityManager is Shiro core architecture, with interior safety components together constitute the security umbrella.
  • Realms: used authority to verify the information, we own realization. Realm is essentially on a specific security DAO: details of its source connected to the data package to obtain the relevant data needed to Shiro. When configuring Shiro, you must specify at least one Realm to implement authentication (authentication) and / or authorization (authorization).

Third, code implementation

1, add Maven relies

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

2, the configuration file

application.yml

# 服务器端口
server:
  port: 8081

# 配置Spring相关信息
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root
    password: root

# 配置Mybatis
mybatis:
  type-aliases-package: com.niaobulashi.model
  mapper-locations: classpath:mapper/*.xml
  configuration:
    # 开启驼峰命名转换
    map-underscore-to-camel-case: true

# 打印SQL日志
logging:
  level:
    com.niaobulashi.mapper: DEBUG

Start scanning method to add mapper, I usually start above method stated otherwise require a separate statement scan on each mapper

@SpringBootApplication
@MapperScan("com.niaobulashi.mapper")
public class ShiroApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShiroApplication.class, args);
    }
}

3, simple table design

Nothing more than five tables: a user table, table role, authority table, table user roles, role permissions table.

Look at this picture below, it can be quite clear.

In particular I will not put out, they take up too much space. . Directly attached link: https: //github.com/niaobulashi/spring-boot-learning/blob/master/spring-boot-20-shiro/db/test.sql

4, the entity class

User.java

@Data
public class User implements Serializable {

    private static final long serialVersionUID = -6056125703075132981L;

    private Integer id;

    private String account;

    private String password;

    private String username;
}

Role.java

@Data
public class Role implements Serializable {

    private static final long serialVersionUID = -1767327914553823741L;

    private Integer id;

    private String role;

    private String desc;
}

5, mapper layer

Here we summarize: simple user login privileges Shiro control related to the database operations are mainly Sa

  • User login name queries user information
  • According to the user query role information
  • 根据角色查询权限信息

UserMapper.java/UserMapper.xml

public interface UserMapper {
    /**
     * 根据账户查询用户信息
     * @param account
     * @return
     */
    User findByAccount(@Param("account") String account);
}
<!--用户表结果集-->
<sql id="base_column_list">
    id, account, password, username
</sql>

<!--根据账户查询用户信息-->
<select id="findByAccount" parameterType="Map" resultType="com.niaobulashi.model.User">
    select
    <include refid="base_column_list"/>
    from user
    where account = #{account}
</select>

RoleMapper.java/RoleMapper.xml

public interface RoleMapper {
    /**
     * 根据userId查询角色信息
     * @param userId
     * @return
     */
    List<Role> findRoleByUserId(@Param("userId") Integer userId);
}
<!--角色表字段结果集-->
<sql id="base_cloum_list">
    id, role, desc
</sql>

<!--根据userId查询角色信息-->
<select id="findRoleByUserId" parameterType="Integer" resultType="com.niaobulashi.model.Role">
    select r.id, r.role
    from role r
    left join user_role ur on ur.role_id = r.id
    left join user u on u.id = ur.user_id
    where 1=1
    and u.user_id = #{userId}
</select>

PermissionMapper.java/PermissionMapper.xml

public interface PermissionMapper {
    /**
     * 根据角色id查询权限
     * @param roleIds
     * @return
     */
    List<String> findByRoleId(@Param("roleIds") List<Integer> roleIds);
}
<!--权限查询结果集-->
<sql id="base_column_list">
    id, permission, desc
</sql>

<!--根据角色id查询权限-->
<select id="findByRoleId" parameterType="List" resultType="String">
    select permission
    from permission, role_permission rp
    where rp.permission_id = permission.id and rp.role_id in
    <foreach collection="roleIds" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</select>

6、Service层

没有其他逻辑,只有继承。

注意:

不过需要注意的一点是,我在Service层中,使用的注解@Service:启动时会自动注册到Spring容器中。

否则启动时,拦截器配置初始化时,会找不到Service。。。这点有点坑。

UserService.java/UserServiceImpl.java

public interface UserService {
    /**
     * 根据账户查询用户信息
     * @param account
     * @return
     */
    User findByAccount(String account);
}
@Service("userService")
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    /**
     * 根据账户查询用户信息
     * @param account
     * @return
     */
    @Override
    public User findByAccount(String account) {
        return userMapper.findByAccount(account);
    }
}

RoleService.java/RoleServiceImpl.java

public interface RoleService {

    /**
     * 根据userId查询角色信息
     * @param id
     * @return
     */
    List<Role> findRoleByUserId(Integer id);
}
@Service("roleService")
public class RoleServiceImpl implements RoleService {

    @Resource
    private RoleMapper roleMapper;

    /**
     * 根据userId查询角色信息
     * @param id
     * @return
     */
    @Override
    public List<Role> findRoleByUserId(Integer id) {
        return roleMapper.findRoleByUserId(id);
    }
}

PermissionService.java/PermissionServiceImpl.java

public interface PermissionService {

    /**
     * 根据角色id查询权限
     * @param roleIds
     * @return
     */
    List<String> findByRoleId(@Param("roleIds") List<Integer> roleIds);
}
@Service("permissionService")
public class PermissionServiceImpl implements PermissionService {

    @Resource
    private PermissionMapper permissionMapper;

    /**
     * 根据角色id查询权限
     * @param roleIds
     * @return
     */
    @Override
    public List<String> findByRoleId(List<Integer> roleIds) {
        return permissionMapper.findByRoleId(roleIds);
    }
}

7、系统统一返回状态枚举和包装方法

状态字段枚举

StatusEnmus.java

public enum StatusEnums {

    SUCCESS(200, "操作成功"),
    SYSTEM_ERROR(500, "系统错误"),
    ACCOUNT_UNKNOWN(500, "账户不存在"),
    ACCOUNT_IS_DISABLED(13, "账号被禁用"),
    INCORRECT_CREDENTIALS(500,"用户名或密码错误"),
    PARAM_ERROR(400, "参数错误"),
    PARAM_REPEAT(400, "参数已存在"),
    PERMISSION_ERROR(403, "没有操作权限"),
    NOT_LOGIN_IN(15, "账号未登录"),
    OTHER(-100, "其他错误");

    @Getter
    @Setter
    private int code;
    @Getter
    @Setter
    private String message;

    StatusEnums(int code, String message) {
        this.code = code;
        this.message = message;
    }
}

响应包装方法

ResponseCode.java

@Data
@AllArgsConstructor
public class ResponseCode<T> implements Serializable {

    private Integer code;
    private String message;
    private Object data;

    private ResponseCode(StatusEnums responseCode) {
        this.code = responseCode.getCode();
        this.message = responseCode.getMessage();
    }

    private ResponseCode(StatusEnums responseCode, T data) {
        this.code = responseCode.getCode();
        this.message = responseCode.getMessage();
        this.data = data;
    }

    private ResponseCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    /**
     * 返回成功信息
     * @param data      信息内容
     * @param <T>
     * @return
     */
    public static<T> ResponseCode success(T data) {
        return new ResponseCode<>(StatusEnums.SUCCESS, data);
    }

    /**
     * 返回成功信息
     * @return
     */
    public static ResponseCode success() {
        return new ResponseCode(StatusEnums.SUCCESS);
    }

    /**
     * 返回错误信息
     * @param statusEnums      响应码
     * @return
     */
    public static ResponseCode error(StatusEnums statusEnums) {
        return new ResponseCode(statusEnums);
    }
}

8、Shiro配置

ShiroConfig.java

@Configuration
public class ShiroConfig {

    /**
     * 路径过滤规则
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSuccessUrl("/");
        // 拦截器
        Map<String, String> map = new LinkedHashMap<>();
        // 配置不会被拦截的链接 顺序判断
        map.put("/login", "anon");
        // 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
        // 进行身份认证后才能访问
        // authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
        map.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * 自定义身份认证Realm(包含用户名密码校验,权限校验等)
     * @return
     */
    @Bean
    public AuthRealm authRealm() {
        return new AuthRealm();
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm());
        return securityManager;
    }

    /**
     * 开启Shiro注解模式,可以在Controller中的方法上添加注解
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

扩展:权限拦截Filter的URL的一些说明

这里扩展一下权限拦截Filter的URL的一些说明

1、URL匹配规则

(1)“?”:匹配一个字符,如”/admin?”,将匹配“ /admin1”、“/admin2”,但不匹配“/admin”

(2)“”:匹配零个或多个字符串,如“/admin”,将匹配“ /admin”、“/admin123”,但不匹配“/admin/1”

(3)“”:匹配路径中的零个或多个路径,如“/admin/”,将匹配“/admin/a”、“/admin/a/b”

2、shiro过滤器

Filter 解释
anon 无参,开放权限,可以理解为匿名用户或游客
authc 无参,需要认证
logout 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
authcBasic 无参,表示 httpBasic 认证
user 无参,表示必须存在用户,当登入操作时不做检查
ssl 无参,表示安全的URL请求,协议为 https
perms[user] 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user, admin"],当有多个参数时必须每个参数都通过才算通过
roles[admin] 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin,user"],当有多个参数时必须每个参数都通过才算通过
rest[user] 根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等
port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数

常用的主要就是 anon,authc,user,roles,perms 等

Note : anon, authc, authcBasic, user authentication is first set of filters, perms, port, rest, roles , ssl second filter group authorization, the authorization through the filter, it must first login authentication operation is completed (i.e., first to complete the certification authorization to go to find) to take the second set of authorized devices (such as access permissions required roles url, if you have not landed, it will jump directly to the shiroFilterFactoryBean.setLoginUrl();url setting).

9, Custom Realm

Main successor AuthorizingRealm, which the method overrides doGetAuthorizationInfo,doGetAuthenticationInfo

Authorization:doGetAuthorizationInfo

Certification:doGetAuthenticationInfo

AuthRealm.java

public class AuthRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    @Resource
    private RoleService roleService;

    @Resource
    private PermissionService permissionService;

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        User user = (User) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 根据用户Id查询角色信息
        List<Role> roleList = roleService.findRoleByUserId(user.getId());
        Set<String> roleSet = new HashSet<>();
        List<Integer> roleIds = new ArrayList<>();
        for (Role role : roleList) {
            roleSet.add(role.getRole());
            roleIds.add(role.getId());
        }
        // 放入角色信息
        authorizationInfo.setRoles(roleSet);
        // 放入权限信息
        List<String> permissionList = permissionService.findByRoleId(roleIds);
        authorizationInfo.setStringPermissions(new HashSet<>(permissionList));

        return authorizationInfo;
    }

    /**
     * 认证
     * @param authToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authToken;
        // 根据用户名查询用户信息
        User user = userService.findByAccount(token.getUsername());
        if (user == null) {
            return null;
        }
        return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
    }
}

10, Contrller layer

@RestController
public class LoginController {

    /**
     * 登录操作
     * @param user
     * @return
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public ResponseCode login(@RequestBody User user) {
        Subject userSubject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(user.getAccount(), user.getPassword());
        try {
            // 登录验证
            userSubject.login(token);
            return ResponseCode.success();
        } catch (UnknownAccountException e) {
            return ResponseCode.error(StatusEnums.ACCOUNT_UNKNOWN);
        } catch (DisabledAccountException e) {
            return ResponseCode.error(StatusEnums.ACCOUNT_IS_DISABLED);
        } catch (IncorrectCredentialsException e) {
            return ResponseCode.error(StatusEnums.INCORRECT_CREDENTIALS);
        } catch (Throwable e) {
            e.printStackTrace();
            return ResponseCode.error(StatusEnums.SYSTEM_ERROR);
        }
    }


    @GetMapping("/login")
    public ResponseCode login() {
        return ResponseCode.error(StatusEnums.NOT_LOGIN_IN);
    }

    @GetMapping("/auth")
    public String auth() {
        return "已成功登录";
    }

    @GetMapping("/role")
    @RequiresRoles("vip")
    public String role() {
        return "测试Vip角色";
    }

    @GetMapping("/permission")
    @RequiresPermissions(value = {"add", "update"}, logical = Logical.AND)
    public String permission() {
        return "测试Add和Update权限";
    }

    /**
     * 登出
     * @return
     */
    @GetMapping("/logout")
    public ResponseCode logout() {
        getSubject().logout();
        return ResponseCode.success();
    }
}

Fourth, the test

1. Log: http: // localhost: 8081 / login

{
    "account":"123",
    "password":"232"
}

2, the other is get request URL sent directly on the line.

Interface testing has passed, it can be safe to eat.


Recommended reading:

Zhang Kaitao old "follow me Shiro" https://jinnianshilongnian.iteye.com/blog/2018936

Guess you like

Origin www.cnblogs.com/niaobulashi/p/springboot-shiro.html