<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.2version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.2version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
<version>1.18.12version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.20version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>${shiro-version}version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>${shiro-version}version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-webartifactId>
<version>${shiro-version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
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 | 0 | 0 |
2 | aaa | 82c1a1ef7dd57d095f3d221e51bd6b16 | 0 | 0 |
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()
);
}
}
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
login(UsernamePasswordToken)
方法,实际上是调用的SecurityManager
的login方法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 | 0 | 0 |
2 | 作家 | writer | 0 | 0 |
sys_permission
id | name | code | url | status | Is_delete |
---|---|---|---|---|---|
1 | 用户查看 | user:view | /user/** | 0 | 0 |
2 | 用户增删改 | user:edit | /user/** | 0 | 0 |
3 | 文章查看 | article:view | /article/** | 0 | 0 |
4 | 文章增删改 | article:edit | /article/** | 0 | 0 |
另外还有用户角色关联表和角色权限关联表
我这里测试的数据是:
admin用户是管理员和作家,aaa用户是作家。
管理员拥有所有权限,作家拥有文章相关的权限。
/**
* 授权
*/
@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
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.10.3version>
dependency>
创建、解析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取代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;
}
}
@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;
}
加入自定义的过滤器和密码匹配器,所有接口都需要执行自定义过滤器。
@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过期刷新也是必不可少的。