源码地址:springboot集成mybatis+shiro+jwt(auth0) 实现无状态登录和鉴权
一、新建springboot工程集成相关依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tongchenggo</groupId>
<artifactId>tongchenggo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tongchenggo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!-- shiro权限控制依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<!--导入连接MySQL的依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.tongchenggo.TongchenggoApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
二、编写实体类user,role.permission三个类及常量类constant。
User
package com.tongchenggo.entity;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/*
* 用户类
* @author jiang
* @date 2021.6.11
* */
@Component
public class User implements Serializable {
private Integer ID;
private String userName;
private String password;
private Integer rid;
private Role role;
public Integer getRid() {
return rid;
}
public void setRid(Integer rid) {
this.rid = rid;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public Integer getID() {
return ID;
}
public void setID(Integer ID) {
this.ID = ID;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Role
package com.tongchenggo.entity;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.List;
/*
* 角色类
* @author jiang
* @date 2021.6.11
* */
@Component
public class Role implements Serializable {
private Integer ID;
private String role;
private List<Permission> permissionList;
public List<Permission> getPermissionList() {
return permissionList;
}
public void setPermissionList(List<Permission> permissionList) {
this.permissionList = permissionList;
}
public Integer getID() {
return ID;
}
public void setID(Integer ID) {
this.ID = ID;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
Permission
package com.tongchenggo.entity;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/*
* 权限类
* @author jiang
* @date 2021.6.11
* */
@Component
public class Permission implements Serializable {
private Integer ID;
private String permission;
public Integer getID() {
return ID;
}
public void setID(Integer ID) {
this.ID = ID;
}
public String getPermission() {
return permission;
}
public void setPermission(String permission) {
this.permission = permission;
}
}
Constant
package com.tongchenggo.constant;
/*
* 常量类
* @author jiang
* @date 2021.6.11
* */
public class Constant {
//失败返回码
public static final Integer FAIL_CODE = 0;
//成功返回码
public static final Integer SUCCESS_CODE = 1;
//token失效返回码
public static final Integer TOKEN_FAILURE_CODE = -1;
//token加密密钥
public static final String SECRET="jiang";
}
三、创建数据库添加user,role,permission表及关联表。
四、实现数据操作接口UserMapper、RoleMapper、PermissionMapper。
UserMapper
package com.tongchenggo.mapper;
import com.tongchenggo.entity.User;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
/*
* 用户表操作接口
* @author jiang
* @date 2021.6.11
* */
@Repository
public interface UserMapper {
@Results(id = "userResultMap",value = {
@Result(column = "uid",property = "ID" ,id = true),
@Result(column = "userName" ,property = "userName"),
@Result(column = "password",property = "password")
})
//通过用户名查询用户、密码
@Select("select * from sys_user u where u.userName=#{userName}")
User getUserByName(String userName);
@Results(id="userInfoResultMap" , value = {
@Result(column = "uid",property = "ID" ,id = true),
@Result(column = "userName" ,property = "userName"),
@Result(column = "password",property = "password"),
@Result(column = "rid",property = "rid"),
@Result(column = "rid",property ="role", one=@One(select = "com.tongchenggo.mapper.RoleMapper.getRoleByrid") ),
})
//通过用户名查询用户、角色
@Select("select * from sys_user u where u.userName=#{userName}")
User getUserRoleByName(String userName);
//添加用户
@Insert("insert into sys_user values(null,#{userName},#{password},#{rid})")
void addUser(User user);
}
RoleMapper
package com.tongchenggo.mapper;
import com.tongchenggo.entity.Role;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
/*
* 角色表操作接口
* @author jiang
* @date 2021.6.11
* */
@Repository
public interface RoleMapper {
@Results(id="roleResultMap" , value = {
@Result(column = "rid",property = "ID" ,id = true),
@Result(column = "role" ,property = "role"),
@Result(column = "rid",property = "permissionList",many = @Many(select = "com.tongchenggo.mapper.PermissionMapper.getPermission"))
})
//角色ID查询用户角色
@Select("select * from sys_role where sys_role.rid=#{rid} ")
Role getRoleByrid(Integer rid);
}
PermissionMapper
package com.tongchenggo.mapper;
import com.tongchenggo.entity.Permission;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
/*
* 权限表操作接口
* @author jiang
* @date 2021.6.11
* */
@Repository
public interface PermissionMapper {
@Results(id = "permissionResultMap",value = {
@Result(column = "pid",property = "ID",id = true),
@Result(column = "permission",property = "permission")
})
//通过角色ID查询该角色拥有的权限
@Select("select * from sys_permission p where p.pid in(select rp.pid from role_permission rp where rp.rid=#{rid})")
List<Permission> getPermission(Integer rid);
}
五、创建service服务接口
UserService
package com.tongchenggo.service;
import com.tongchenggo.entity.User;
import com.tongchenggo.vo.ResultVO;
/*
* 用户相关服务接口
* @author jiang
* @date 2021.6.11
* */
public interface UserService {
//登录
ResultVO login(String userName, String password);
//注册
ResultVO register(String userName,String password);
//退出
ResultVO logout();
//根据用户名获取用户
User getUserByName(String userName);
}
六、service接口实现
UserServiceImpl
package com.tongchenggo.serviceImpl;
import com.tongchenggo.constant.Constant;
import com.tongchenggo.utils.MD5Utils;
import com.tongchenggo.vo.ResultVO;
import com.tongchenggo.entity.User;
import com.tongchenggo.utils.JWTUtil;
import com.tongchenggo.mapper.UserMapper;
import com.tongchenggo.service.UserService;
import com.tongchenggo.vo.Token;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/*
* 用户相关服务接口实现
* @author jiang
* @date 2021.6.11
* */
@Service
public class UserServiceImpl implements UserService {
public static Integer ROLE=1;
@Autowired
private UserMapper userMapper;
/*
* 用户登录
* 返回登录状态,成功携带token,失败返回失败信息
* */
@Override
public ResultVO login(String userName, String password) {
//判断用户账号或密码是否为空
if(userName==null|"".equals(userName)|password==null|"".equals(password)){
return ResultVO.fail(Constant.FAIL_CODE,"账号或密码不能为空");
}
//根据用户名密码创建token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,password);
usernamePasswordToken.setRememberMe(false);
//获取认证主体subject
Subject subject = SecurityUtils.getSubject();
try{
//根据用户信息登录
subject.login(usernamePasswordToken);
}catch (Exception e){
return ResultVO.fail(Constant.FAIL_CODE,"登录失败");
}
String token = null;
try {
token=JWTUtil.sign(userName);
}catch (Exception e){
e.getStackTrace();
}
return ResultVO.success("登录成功",new Token(token));
}
/*
* 用户注册
* 返回注册状态
* */
@Override
public ResultVO register(String userName, String password) {
//判断用户账号、密码、角色、权限是否为空
if(userName==null|"".equals(userName)|password==null|"".equals(password)){
return ResultVO.fail(Constant.FAIL_CODE,"账号、密码、角色、权限任一一项不可为空");
}
//根据用户名查询用户判断是否存在该用户
User user = userMapper.getUserByName(userName);
if(user==null){
user = new User();
user.setUserName(userName);
user.setPassword(MD5Utils.encryptionPassword(password,userName));
user.setRid(ROLE);
try {
userMapper.addUser(user);//添加用户
}catch (Exception e){
return ResultVO.fail(Constant.FAIL_CODE,"注册失败");
}
return ResultVO.success("注册成功");
}else {
return ResultVO.fail(Constant.FAIL_CODE,"用户已存在");
}
}
/*
* 用户退出
* 返回退出结果
* */
public ResultVO logout(){
SecurityUtils.getSubject().logout();
return ResultVO.success("退出成功!");
}
/*
* 根据用户名获取用户
* */
public User getUserByName(String userName){
return userMapper.getUserRoleByName(userName);
}
}
七、编写shiro相关配置及自定义实现类和JWT相关工具。
7.1、shiro配置类
ShiroConfig
package com.tongchenggo.shiro;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
/*
* shiro配置类
* @author jiang
* @date 2021.6.11
* */
@Configuration
public class ShiroConfig {
/*
* 配置密码匹配器
* */
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(10);
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
/*
* 配置自定义realm,设置密码匹配器
* */
@Bean
public PasswordRealm passwordRealm(HashedCredentialsMatcher hashedCredentialsMatcher){
PasswordRealm passwordRealm = new PasswordRealm();
passwordRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return passwordRealm;
}
@Bean
public JwtRealm jwtRealm(HashedCredentialsMatcher hashedCredentialsMatcher){
JwtRealm jwtRealm = new JwtRealm();
jwtRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return jwtRealm;
}
/*
* 配置安全管理器,设置自定义realm,禁用session
*/
@Bean
public SecurityManager securityManager(PasswordRealm passwordRealm, JwtRealm jwtRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
Collection reams = new ArrayList();
reams.add(passwordRealm);
reams.add(jwtRealm);
securityManager.setRealms(reams);
//禁用session
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
defaultSubjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(defaultSubjectDAO);
return securityManager;
}
/*
* @author jiang
* @date 2021.6.11
*
* 配置shiro拦截器
* shiro内置过滤器,可以实现权限相关拦截
* 常用过滤器:
* anon:无需认证,即无需登录就可以访问
* authc:必须认证才可以访问
* user:如果使用rememberMe的功能才可以直接访问
* perms:必须拥有perms[xx]权限才可以访问
* roles:必须拥有roles[xx]权限才可以访问
* */
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//添加自定义拦截器filter
shiroFilterFactoryBean.getFilters().put("jwt", new JwtFilter());
//身份认证失败跳转到登录页面
//该方法需要使用authc才会生效,这里使用使用自定义拦截器jwt验证身份,所以该方法不会生效
shiroFilterFactoryBean.setLoginUrl("/error/unlogin");
//身份认证成功跳转
//前后分离项目中前端会用自己的逻辑在登录成功后进行跳转
// shiroFilterFactoryBean.setSuccessUrl("/ok");
//未授权页面
//该方法需要roles或者perms拦截器才会生效,若使用自定义拦截器依然不会生效
shiroFilterFactoryBean.setUnauthorizedUrl("/error/unauthorized");
//创建拦截链集合,LinkedHashMap有序集合
Map<String,String> filterMap = new LinkedHashMap<String,String>();
//定义拦截规则
//anon无需认证,jwt为自定义拦截器
filterMap.put("/user/add/**","anon");
filterMap.put("/user/post/**","anon");
filterMap.put("/user/get01","jwt");
filterMap.put("/user/get02","jwt,perms[add]");
filterMap.put("/user/get03","jwt,roles[admin]");
filterMap.put("/user/get04","jwt,roles[admin],perms[delete]");
filterMap.put("/user/get05","jwt,roles[ordinary]");
filterMap.put("/user/get06","jwt,roles[ordinary],perms[add]");
filterMap.put("/user/get07","jwt,roles[ordinary],perms[find]");
filterMap.put("/user/**", "jwt");
//注入拦截链到拦截器
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
//注入安全管理器到拦截器
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
}
配置安全管理器时,需要设置禁用session,防止缓存用户信息。
配置拦截器时需要将自定义的拦截器添加到拦截器工厂,实现自己的身份认证过程,shiro默认的拦截器中常用的有:
身份认证:authc、user、anon。
授权:roles、perms。
配置拦截规则时,首先需要配置自定义身份认证拦截器+授权拦截器,以逗号“,”隔开,如
filterMap.put("/user/get03","jwt,roles[admin]");
若不添加jwt(前面添加拦截器时取的别名)时,在授权鉴权时会默认走authc过滤器,则在第一次登录后缓存了用户身份,后续鉴权时即使未携带token或者携带错误token访问,也会直接通过身份认证;而添加jwt后,在首次用户密码登录后,返回给用户token,用户访问服务时携带token首先需要通过自定义的身份认证过程,再进行授权鉴权过程最后确认用户是否有权访问资源。
7.2、自定义JwtToken实现AuthenticationToken。
JwtToken
package com.tongchenggo.shiro;
import org.apache.shiro.authc.AuthenticationToken;
/*
* 自定义token
* @author jiang
* @date 2021.6.11
* */
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
//将token作为返回值
@Override
public String getPrincipal() {
return this.token;
}
@Override
public String getCredentials() {
return this.token;
}
}
7.3、自定义realm,这里重写了2个自定义realm类实现不同的校验方式(密码校验和token校验)。
PasswordRealm
package com.tongchenggo.shiro;
import com.tongchenggo.exception.MyShiroException;
import com.tongchenggo.entity.User;
import com.tongchenggo.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
/*
* 密码验证类
* @author jiang
* @date 2021.6.11
* */
public class PasswordRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/*
*subject.login(token)方法中的token是UsernamePasswordToken时,调用此Realm的doGetAuthenticationInfo
* 必须重写此方法
* */
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
//用户认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("PasswordRealm执行");
//通过authenticationToken获取用户名
String userName = (String) authenticationToken.getPrincipal();
//根据用户名从数据库查询用户信息
User user = userService.getUserByName(userName);
//判断用户是否存在
if (user != null) {
//认证用户信息
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUserName(), user.getPassword(), "passwordRealm");
//设置盐值加密
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(userName));
return authenticationInfo;
} else {
throw new MyShiroException("用户不存在");
}
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}
JwtRealm
package com.tongchenggo.shiro;
import com.tongchenggo.entity.Permission;
import com.tongchenggo.entity.User;
import com.tongchenggo.exception.MyShiroException;
import com.tongchenggo.service.UserService;
import com.tongchenggo.utils.JWTUtil;
import com.tongchenggo.constant.Constant;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
/*
* token校验类
* @author jiang
* @date 2021.6.11
* */
public class JwtRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/*
*subject.login(token)方法中的token是JwtToken时,调用此Realm的doGetAuthenticationInfo
* 必须重写此方法
* */
@Override
public boolean supports(AuthenticationToken token) {
return token!=null && token instanceof JwtToken;
}
/*
* 校验token身份认证
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("JWTRealm执行");
String token =(String)authenticationToken.getPrincipal();
String userName = JWTUtil.getUsername(token);
if(!JWTUtil.verify(token,userName)){
throw new MyShiroException(Constant.TOKEN_FAILURE_CODE, "token无效,请重新登录");
}else {
//根据用户名从数据库查询用户信息
User user = userService.getUserByName(userName);
//判断用户是否存在
if(user!=null){
//返回给客户端的token未加密,在shiro里比对时会根据规则加密后比对
//用户密码登录会根据用户输入密码加密后与数据库里加密过的密码对比
SimpleHash encryptionToken = new SimpleHash("MD5",
token, ByteSource.Util.bytes(userName),
10);
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,encryptionToken,ByteSource.Util.bytes(userName),"jwtRealm");
return authenticationInfo;
}else{
throw new MyShiroException("token非法");
}
}
}
/*
* 授权
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User user = userService.getUserByName(principalCollection.getPrimaryPrincipal().toString());
List<String> perms = new ArrayList<String>();
if(!CollectionUtils.isEmpty(user.getRole().getPermissionList())){
for(Permission permission : user.getRole().getPermissionList()){
perms.add(permission.getPermission());
}
}
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole(user.getRole().getRole());
simpleAuthorizationInfo.addStringPermissions(perms);
return simpleAuthorizationInfo;
}
}
重写realm类时一定要重写supports方法,在多realm环境或者自定义实现AuthenticationToken时,需要根据传入的token类型比较调用对应realm里的身份认证和授权方法。
7.4、自定义拦截器JwtFilter继承BasicHttpAuthenticationFilter
package com.tongchenggo.shiro;
import com.tongchenggo.constant.Constant;
import com.tongchenggo.vo.ResultVO;
import com.tongchenggo.utils.ShiroSendMessageUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
/*
* @author jiang
* @date 2021.6.11
* 自定义身份认证拦截器
* 认证流程:
* OncePerRequestFilter#doFilter=>AdviceFilter#doFilterInternal=>PathMatchingFilter#preHandle=>AccessControlFilter#onPreHandle
* AccessControlFilter#onPreHandle会调用isAccessAllowed和onAccessDenied方法来确认是否通过认证
* public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
* return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
* }
* */
public class JwtFilter extends BasicHttpAuthenticationFilter {
/*
* 重写createToken方法
* 创建自定义的JwtToken
* */
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
// 获取请求头Authorization的值
String authorization = getAuthzHeader(request);
return new JwtToken(authorization);
}
/*
* 重写isLoginAttempt方法
* 判断用户是否想要登录。
* 检测header里面是否包含Authorization字段,即token
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("Authorization");
return authorization != null;
}
/*
* 重写executeLogin方法
* 执行登录操作
* 登录异常里发送自定义异常信息给用户
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = this.createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
} else {
try {
Subject subject = this.getSubject(request, response);
subject.login(token);
return this.onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
ResultVO resultVO = ResultVO.fail("用户认证失败,请确认是否登录或登录已超时");
ShiroSendMessageUtils.sendResponse(response, resultVO);
return this.onLoginFailure(token, e, request, response);
}
}
}
/*
* 重写onAccessDenied
* isAccessAllowed身份认证未通过,执行此方法
* 返回true,请求一定会通过
* 返回false,结束过滤链
* 这里直接返回false处理错误提示
*
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String authorization = getAuthzHeader(request);
if (StringUtils.isEmpty(authorization)){
ShiroSendMessageUtils.sendResponse(response, ResultVO.fail(Constant.TOKEN_FAILURE_CODE,"请求未携带token,请确认是否登录!"));
}
return false;
}
/*
* 重写isAccessAllowed方法
* 是否允许访问
* 检查未携带token拒绝访问,在onAccessDenied方法里发送无token信息给用户,有token调用登录请求
* */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
boolean loggedIn=false;
if (isLoginAttempt(request, response)) {
try {
loggedIn = executeLogin(request, response);
} catch (Exception e) {
loggedIn = false;
}
}
return loggedIn;
}
// @Override
// protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
// HttpServletRequest req =(HttpServletRequest)request;
// HttpServletResponse res = (HttpServletResponse)response;
// res.setHeader("Access-control-Allow-Origin",req.getHeader("Origin"));
// res.setHeader("Access-Control-Allow-Methods","GET,POST,OPTIONS,PUT,DELETE");
// res.setHeader("Access-Control-Allow-Headers",req.getHeader("Access-Control-Request-Headers"));
// if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
// res.setStatus(HttpStatus.OK.value());
// return false;
// }
// return super.preHandle(request, response);
// }
}
自定义拦截器里一定要重写createToken方法,返回我们自定义的JwtToken,实现用户访问需要授权的资源时,走自定义拦截器JwtFilter时可以获取到用户携带的JwtToken,通过JwtRealm校验token以确认权限。
shiro在身份认证过程中会通过onPreHandle()方法调用isAccessAllowed和onAccessDenied来确认是否通过认证,其中一个方法返回true即可通过身份认证,我们在isAccessAllowed方法里来确认是否通过身份认证,在onAccessDenied直接返回false,并返回一些身份认证未通过的信息给用户。
7.5、编写自定义异常类。
MyException
package com.tongchenggo.exception;
/*
* 自定义异常
* @author jiang
* @date 2021.6.11
* */
public class MyException extends RuntimeException {
private Integer code = 0;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public MyException(String message) {
super(message);
}
public MyException(Integer code, String message) {
super(message);
this.code = code;
}
}
MyShiroException
package com.tongchenggo.exception;
import org.apache.shiro.authc.AuthenticationException;
/*
* 自定义shiro相关异常
* @author jiang
* @date 2021.6.11
* */
public class MyShiroException extends AuthenticationException {
private Integer code=0;
public MyShiroException(String message) {
super(message);
}
public MyShiroException(Integer code, String message) {
super(message);
this.code = code;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
7.6、生成token的工具类JWTUtil、密码加密工具类MD5Utils、发送异常信息工具类ShiroSendMessageUtils
JWTUtil
package com.tongchenggo.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.tongchenggo.constant.Constant;
import java.io.UnsupportedEncodingException;
import java.util.Date;
/*
* token生成、验证工具类
* @author jiang
* @date 2021.6.11
* */
public class JWTUtil {
//有效期
private static final long EXPIRE_TIME = 60*1000;
//密钥
private static final String SECRET = Constant.SECRET;
/*
* 校验token是否正确
*/
public static boolean verify(String token, String username) {
try {
//根据密钥生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
//效验token
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
/*
* 获得token中的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/*
* 生成签名
*/
public static String sign(String username) throws UnsupportedEncodingException {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
Date date1 = new Date();
return JWT.create()
.withIssuer("auth0")//签发人
.withClaim("username", username)//载体数据
.withIssuedAt(date1)//签发时间
.withExpiresAt(date)//过期时间
.sign(algorithm);//生成新的JWT
}
}
MD5Utils
package com.tongchenggo.utils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
/*
* 密码加密类
* @author jiang
* @date 2021.6.11
* */
public class MD5Utils {
public static String encryptionPassword(String password,String salt){
return new SimpleHash("MD5",
password, ByteSource.Util.bytes(salt),
10).toString();
}
}
ShiroSendMessageUtils
package com.tongchenggo.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tongchenggo.vo.ResultVO;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/*
* Shiro发送异常信息工具类
* @author jiang
* @date 2021.6.11
* */
public class ShiroSendMessageUtils {
public static ObjectMapper objectMapper = new ObjectMapper();
/*
* response发送响应数据ResultVO
*/
public static void sendResponse(ServletResponse response, ResultVO resultVO) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try (PrintWriter out = response.getWriter()){
out.print(objectMapper.writeValueAsString(resultVO));
}catch (Exception e){
throw e;
}
}
}
八、统一响应结果类
ResultVO
package com.tongchenggo.vo;
import com.tongchenggo.constant.Constant;
import java.io.Serializable;
/*
* 数据响应封装类
* @author jiang
* @date 2021.6.11
* */
public class ResultVO<T> implements Serializable {
//响应状态
private int status;
//响应信息描述
private String message;
//响应数据
private T data;
public ResultVO(int status, String message){
this.status = status;
this.message = message;
}
public ResultVO(int status, String message, T data) {
this.status = status;
this.message = message;
this.data = data;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static<T> ResultVO<T> success(String message,T data){
return new ResultVO(Constant.SUCCESS_CODE,message,data);
}
public static<T> ResultVO<T> success(String message){
return new ResultVO(Constant.SUCCESS_CODE,message);
}
public static ResultVO fail(String message){
return new ResultVO(Constant.FAIL_CODE, message, null);
}
public static ResultVO fail(int status, String message){
return new ResultVO(status, message, null);
}
}
Token
package com.tongchenggo.vo;
import lombok.Data;
/*
* @author jiang
* @date 2021.6.11
* */
@Data
public class Token {
private String token;
public Token(String token){
this.token = token;
}
}
九、访问控制器编写
UserController
package com.tongchenggo.controller;
import com.tongchenggo.vo.ResultVO;
import com.tongchenggo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/*
* 用户注册、登录
* 测试权限控制
* @author jiang
* @date 2021.6.11
* */
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/add/{userName}/{password}")
public ResultVO register(@PathVariable String userName,@PathVariable String password){
return userService.register(userName,password);
}
@RequestMapping("/post/{userName}/{password}")
public ResultVO login(@PathVariable String userName,@PathVariable String password){
return userService.login(userName,password);
}
@RequestMapping("/get07")
public ResultVO get07(){
return ResultVO.success("已认证普通用户,有查询权限");
}
@RequestMapping("/get06")
public ResultVO get06(){
return ResultVO.success("已认证,身份为管理员或普通用户,且需有添加权限才可访问");
}
@RequestMapping("/get05")
public ResultVO get05(){
return ResultVO.success("已认证普通用户");
}
@RequestMapping("/get04")
public ResultVO get04(){
return ResultVO.success("已认证且是管理员角色拥有删除权限");
}
@RequestMapping("/get03")
public ResultVO get03(){
return ResultVO.success("已认证且是管理员角色");
}
@RequestMapping("/get02")
public ResultVO get02(){
return ResultVO.success("已认证用户且有添加权限");
}
@RequestMapping("/get01")
public ResultVO get01(){
return ResultVO.success("已认证用户");
}
@RequestMapping("/logout")
public ResultVO logout(){
return userService.logout();
}
}
ErrorController
package com.tongchenggo.controller;
import com.tongchenggo.constant.Constant;
import com.tongchenggo.vo.ResultVO;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/*
* shiro身份未认证和未授权转发控制器
* @author jiang
* @date 2021.6.11
* */
@RestController
@RequestMapping("error")
public class ErrorController {
@RequestMapping("/unauthorized")
public ResultVO unauthorized(){
return ResultVO.fail(Constant.FAIL_CODE,"无访问权限");
}
@RequestMapping("/unlogin")
public ResultVO unlogin(){
return ResultVO.fail(Constant.FAIL_CODE,"未登录用户");
}
}
十、postman测试。
10.1、登录aa用户。