shiro
简介
- Subject:主体
- 代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;
- SecurityManager:安全管理器
- 即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制。
- Realms:域
- Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
登录认证代码实现
pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro-version}</version>
</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>
实体
用户实体
@Data
@TableName("sys_user")
public class SysUser {
private int id;
private String username;
private String password;
private int status;
private int isDelete;
}
用户数据库
id | username | password | status | is_delete |
---|---|---|---|---|
1 | admin | 123 | ||
2 | aaa | 82c1a1ef7dd57d095f3d221e51bd6b16 |
自定义realm
public class MyRealm extends AuthorizingRealm {
@Autowired
private SysUserService userService;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 登录认证
* @param authenticationToken 封装的token(UsernamePasswordToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (authenticationToken.getPrincipal() == null) {
throw new AuthenticationException("token不合法");
}
String username = authenticationToken.getPrincipal().toString();
log.info("用户名:{}", username);
SysUser one = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
if (one == null) {
// 没有该用户名
log.error("没有该用户名: {}", username);
throw new AuthenticationException("用户不存在");
}
// 用户状态判断
if (one.getStatus() == 1) {
throw new AccountException("用户被禁用");
}
if (one.getIsDelete() == 1) {
throw new AccountException("用户被删除");
}
// 其他业务判断
// 判断通过后,将数据库中查询出来的user封装为info
return new SimpleAuthenticationInfo(
one.getUsername(),// 这个参数是什么,在后续的subject.getPrincipal就是什么,也可以设置用户实体
one.getPassword(),
// ByteSource.Util.bytes(one.getUsername()),// 密码加密的"盐值",可以是username、id等
getName()
);
}
}
shiro配置类
public class ShiroConfig {
@Bean("credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 密码加密算法
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
// 加密次数
hashedCredentialsMatcher.setHashIterations(512);
return hashedCredentialsMatcher;
}
/**
* 自定义realm
*/
@Bean("myRealm")
public MyRealm myRealm(@Qualifier("credentialsMatcher") HashedCredentialsMatcher credentialMatcher) {
MyRealm myRealm = new MyRealm();
// myRealm.setCredentialsMatcher(credentialMatcher);
return myRealm;
}
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm);
/*
* 关闭shiro自带的session,详情见文档
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
filter.setSecurityManager(securityManager);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 路径匹配的顺序就是put进去的顺序(最先匹配原则)
// login接口不需要认证
filterChainDefinitionMap.put("/auth/login", "anon");
// getInfo需要认证
filterChainDefinitionMap.put("/auth/getInfo", "authc");
filterChainDefinitionMap.put("/**", "authc");
filter.setLoginUrl("/auth/login");
filter.setSuccessUrl("/auth/getInfo");
filter.setUnauthorizedUrl("/auth/error");
filter.setFilterChainDefinitionMap(filterChainDefinitionMap);
return filter;
}
/**注册shiro的Filter 拦截请求*/
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean(SecurityManager securityManager) throws Exception {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter((Filter) Objects.requireNonNull(this.shiroFilterFactoryBean(securityManager).getObject()));
filterRegistrationBean.addInitParameter("targetFilterLifecycle","true");
//bean注入开启异步方式
filterRegistrationBean.setAsyncSupported(true);
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST);
return filterRegistrationBean;
}
/**
* shiro声明周期
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
// 以下配置开启shiro注解(@RequiresPermissions)
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 启用shiro注解
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
登录业务
public class AuthServiceImpl implements AuthService {
@Override
public String login(SysUser sysUser) {
// 非空判断
if (sysUser == null) {
return null;
}
if (sysUser.getUsername() == null || sysUser.getPassword() == null) {
return null;
}
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
sysUser.getUsername(),
sysUser.getPassword()
);
try {
subject.login(usernamePasswordToken);
} catch (AuthenticationException e) {
log.error("登录失败");
throw new AuthenticationException("登录失败");
}
return "登录成功";
}
}
接口
@RestController
@RequestMapping("auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("login")
public String login(@RequestBody SysUser sysUser) {
return authService.login(sysUser);
}
@GetMapping("getInfo")
public String getInfo() {
return SecurityUtils.getSubject().getPrincipal().toString();
}
@RequestMapping("error")
public String error() {
return "fail";
}
}
测试

当直接访问
auth/getInfo
时,可以看到,无法访问该接口,并且会自动跳转到登录接口(
auth/login
)。
使用用户名和密码登录
然后再访问
auth/getInfo
接口
现在可以正确获取到信息,可以看到获取到的
SecurityUtils.getSubject().getPrincipal()
,就是在reamlm中返回的info设置的
principal
参数。
注:如果要使用加密,则要在shiro配置类里,给自定义的realm设置密码匹配器
@Bean("credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 密码加密算法
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
// 加密次数
hashedCredentialsMatcher.setHashIterations(512);
return hashedCredentialsMatcher;
}
/**
* 自定义realm
*/
@Bean("myRealm")
public MyRealm myRealm(@Qualifier("credentialsMatcher") HashedCredentialsMatcher credentialMatcher) {
MyRealm myRealm = new MyRealm();
myRealm.setCredentialsMatcher(credentialMatcher);
return myRealm;
}
自定义realme里面最后返回的info需要带上加密的“盐值”
return new SimpleAuthenticationInfo(
one.getUsername(),
one.getPassword(),
ByteSource.Util.bytes(one.getUsername()),
getName()
);
总结
登录认证的流程:
- 将前端传来的用户名和密码封装成
UsernamePasswordToken
- 调用Subject的
方法,实际上是调用的login(UsernamePasswordToken)
的login方法SecurityManager
- 进入自定义realme方法
- 最终由定义的密码匹配器进行密码匹配
SimpleCredentialsMatcher源码:默认使用该匹配器,即不加密
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = this.getCredentials(token);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenCredentials, accountCredentials);
}
// token:前端封装的用户名密码token
// info:realme中返回的info
实际就是把数据库中的密码和前端输入的密码进行对比
HashedCredentialsMatcher源码:加密的密码匹配器
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = this.hashProvidedCredentials(token, info);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenHashedCredentials, accountCredentials);
}
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
Object salt = null;
if (info instanceof SaltedAuthenticationInfo) {
salt = ((SaltedAuthenticationInfo)info).getCredentialsSalt();
} else if (this.isHashSalted()) {
salt = this.getSalt(token);
}
return this.hashProvidedCredentials(token.getCredentials(), salt, this.getHashIterations());
}
// 在这里实现的加密
protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
String hashAlgorithmName = this.assertHashAlgorithmName();
return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
}
把前端输入的明文密码,用设置的盐值和加密次数进行加密后,再与数据库中的密文密码进行对比。
授权代码实现
授权就需要实现自定义realme中的
doGetAuthorizationInfo
方法
数据库
sys_role
id | name | code | staus | Is_delete |
---|---|---|---|---|
1 | 管理员 | admin | ||
2 | 作家 | writer |
sys_permission
id | name | code | url | status | Is_delete |
---|---|---|---|---|---|
1 | 用户查看 | user:view | /user/** | ||
2 | 用户增删改 | user:edit | /user/** | ||
3 | 文章查看 | article:view | /article/** | ||
4 | 文章增删改 | article:edit | /article/** |
另外还有用户角色关联表和角色权限关联表
我这里测试的数据是:
admin用户是管理员和作家,aaa用户是作家。
管理员拥有所有权限,作家拥有文章相关的权限。
修改自定义realme
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String username = principalCollection.getPrimaryPrincipal().toString();
SysUser sysUser = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
// 去数据库中查询该用户的角色和权限
List<SysRole> roles = roleService.getRoles(sysUser.getId());
List<SysPermission> permissions = permissionService.getPermissions(sysUser.getId());
Set<String> permissionSet = new HashSet<>();
Set<String> roleSet = new HashSet<>();
permissions.forEach(item -> permissionSet.add(item.getCode()));
roles.forEach(item -> roleSet.add(item.getCode()));
authorizationInfo.addRoles(roleSet);
authorizationInfo.addStringPermissions(permissionSet);
return authorizationInfo;
}
当遇到需要鉴权的时候,会走
doGetAuthorizationInfo
方法
测试
接口
@RequiresPermissions("user:view")
@GetMapping("userView")
public String userView() {
return "用户查看";
}
@RequiresPermissions("article:view")
@GetMapping("articleView")
public String articleView() {
return "文章查看";
}
@RequiresRoles("admin")
@GetMapping("admin")
public String admin() {
return "管理员";
}
登录用户是
aaa
的时候,只能访问
articleView
接口,其他接口均无相应的权限。
访问articleView接口,可以成功访问
当访问其他两个接口时,控制台报错无权调用此方法
org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method
整合JWT
准备工作
pom
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
jwt工具类
创建、解析token
@Slf4j
@Component
public class JwtUtil {
private static final String secret = "secret";
/**
* 创建token
*/
public static String createToken(String username, Long time) throws UnsupportedEncodingException {
long expiration = System.currentTimeMillis() + time;
Date expireDate = new Date(expiration);
String token = JWT.create()
.withClaim("sys_username", username)
.withExpiresAt(expireDate)
.sign(Algorithm.HMAC256(secret));
log.info("用户:{} =====> token:{}", username, token);
return token;
}
/**
* 校验token是否正确
*/
public static boolean verify(String token, String username) throws UnsupportedEncodingException, TokenExpiredException {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("sys_username", username)
.build();
verifier.verify(token);
return true;
}
/**
* 解析token,获取用户名
*/
public static String getUsername(String token) {
DecodedJWT decode = JWT.decode(token);
return decode.getClaim("sys_username").asString();
}
}
自定义token
用自定义的token取代shiro中的token,例如前面的UsernamePasswordToken
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
自定义密码匹配器
public class JwtCredentialsMatcher extends HashedCredentialsMatcher {
/**
* @param info realme中返回的是username,所以getPrincipals()获取的是用户名
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo info) {
JwtToken jwtToken = (JwtToken) authenticationToken;
String token = jwtToken.getCredentials().toString();
try {
return JwtUtil.verify(token, info.getPrincipals().toString());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("token解析失败");
} catch (TokenExpiredException e) {
throw new RuntimeException("token过期");
}
}
}
自定义过滤器
public class JwtFilter extends AccessControlFilter {
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 是否允许访问
* isAccessAllowed返回false后,去执行onAccessDenied方法
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnsupportedEncodingException {
Subject subject = SecurityUtils.getSubject();
String token = HttpUtil.getToken((HttpServletRequest) request);
if (StringUtils.isNotBlank(token)) {
JwtToken jwtToken = new JwtToken(token);
try {
subject.login(jwtToken);
return true;// 登录成功
} catch (Exception e) {
// 登录失败
throw new RuntimeException("登录失败");
}
} else {
// 没有token
return false;
}
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
String token = HttpUtil.getToken((HttpServletRequest) servletRequest);
if (StringUtils.isNotBlank(token)) {
String username = JwtUtil.getUsername(token);
if (JwtUtil.verify(token, username)) {
// 没有权限
}
} else {
// 没有token
}
return false;
}
}
修改shiro配置类
@Bean("jwtFilter")
public JwtFilter jwtFilter() {
return new JwtFilter();
}
@Bean("credentialsMatcher")
public JwtCredentialsMatcher credentialsMatcher() {
return new JwtCredentialsMatcher();
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
filter.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", new JwtFilter());
filter.setFilters(filterMap);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// login接口不需要认证
filterChainDefinitionMap.put("/auth/login", "anon");
filterChainDefinitionMap.put("/**", "jwt");
filter.setFilterChainDefinitionMap(filterChainDefinitionMap);
return filter;
}
加入自定义的过滤器和密码匹配器,所有接口都需要执行自定义过滤器。
修改自定义realme
@Slf4j
public class MyRealm extends AuthorizingRealm {
@Autowired
private SysUserService userService;
@Autowired
private SysRoleService roleService;
@Autowired
private SysPermissionService permissionService;
// 不写该方法,会报错不支持自定义的token
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String username = principalCollection.getPrimaryPrincipal().toString();
SysUser sysUser = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
// 去数据库中查询该用户的角色和权限
List<SysRole> roles = roleService.getRoles(sysUser.getId());
List<SysPermission> permissions = permissionService.getPermissions(sysUser.getId());
Set<String> permissionSet = new HashSet<>();
Set<String> roleSet = new HashSet<>();
permissions.forEach(item -> permissionSet.add(item.getCode()));
roles.forEach(item -> roleSet.add(item.getCode()));
authorizationInfo.addRoles(roleSet);
authorizationInfo.addStringPermissions(permissionSet);
return authorizationInfo;
}
/**
* 登录认证
* @param authenticationToken 自定义的token
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// authenticationToken就是JwtToken
if (authenticationToken.getPrincipal() == null) {
throw new AuthenticationException("token不合法");
}
String token = authenticationToken.getPrincipal().toString();
String username = JwtUtil.getUsername(token);
SysUser one = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
if (one == null) {
// 没有该用户名
log.error("没有该用户名: {}", username);
throw new AuthenticationException("用户不存在");
}
// 用户状态判断
if (one.getStatus() == 1) {
throw new AccountException("用户被禁用");
}
if (one.getIsDelete() == 1) {
throw new AccountException("用户被删除");
}
// 其他业务判断
// 判断通过后,将数据库中查询出来的user封装为info
return new SimpleAuthenticationInfo(
one.getUsername(),// 这个参数是什么,在后续的subject.getPrincipal就是什么
one.getPassword(),
ByteSource.Util.bytes(one.getUsername()),// 密码加密的"盐值",可以是username、id等
getName()
);
}
}
修改登录代码
@Override
public String login(SysUser sysUser) {
// 非空判断
if (sysUser == null) {
return null;
}
if (sysUser.getUsername() == null || sysUser.getPassword() == null) {
return null;
}
try {
String token = JwtUtil.createToken(sysUser.getUsername(), 1440000L);
Subject subject = SecurityUtils.getSubject();
JwtToken jwtToken = new JwtToken(token);
subject.login(jwtToken);
return token;
} catch (AuthenticationException e) {
log.error("登录失败");
throw new AuthenticationException("登录失败");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "登录失败";
}
测试
登录成功后,会返回token。
携带token调用接口,成功返回数据
访问不具备权限的接口
控制台报错:
org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method: public java.lang.String com.shiro.controller.AuthController.userView()
总结
到此,shiro部分和shiro整合jwt部分完成。这只是一个例子,代码中还有很多需要完善的地方,比如:返回结果的封装、对异常的处理等。
另外一般在系统中,token过期刷新也是必不可少的。