shiro Apacher开源的java安全框架,提供了鉴权、认证、加密、会话管理等功能,相对于security,shiro简单很多。
MD5配置
shiro三个最重要的组件
Subject 主题,任何与当前应用交互的东西,所有subject均绑定securityManager,subject相当于门面,实际处理仍是securityManager。
SecurityManager 安全的核心,与后端任何的交互均走securityManager,相当于SpringMVC的DispatcherServlet。
Realm 鉴权+认证,shiro从Realm中取数据。
1、登录与登出
@PostMapping(LoginController.URI)
public Object login(String account, String password) {
if (StringUtils.isBlank(account) || StringUtils.isBlank(password)) {
return "账户或密码不能为空!";
}
UsernamePasswordToken userToken = new UsernamePasswordToken(account, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(userToken);
Session session = subject.getSession();
SysUser user = (SysUser) subject.getPrincipal();
String token = (String) session.getId();
return token;
} catch (UnknownAccountException e) {
log.error("账户不存在:{}", e.getMessage());
return "账户不存在";
} catch (IncorrectCredentialsException e) {
log.error("密码错误:{}", e.getMessage());
return "密码错误";
} catch (LockedAccountException e) {
log.error("账户已锁定:{}", e.getMessage());
return "账户已锁定";
// } catch (ExcessiveAttemptsException eae) {
// return "用户名或密码错误次数过多";
} catch (AuthenticationException e) {
log.error("验证失败:{}", e.getMessage());
return "验证失败";
}
}
@GetMapping(LoginController.URI + "/logout")
public Object logout() {
Subject subject = SecurityUtils.getSubject();
if (subject != null && (subject.getPrincipal() != null)) {
log.info("logout token:{}", subject.getSession().getId());
subject.getPrincipal();
subject.logout();
}
return "登出成功!";
}
2、首先配置ShiroConfig的加密方式、迭代次数等
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(1024);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
2、在ShiroConfig中,加入到鉴权认证中
@Bean("customAuthorizingRealm")
public CustomAuthorizingRealm customAuthorizingRealm() {
CustomAuthorizingRealm customAuthorizingRealm = new CustomAuthorizingRealm();
customAuthorizingRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return customAuthorizingRealm;
}
3、在鉴权认证CustomAuthorizingRealm中进行盐值的设置
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (authenticationToken.getPrincipal() == null) {
return null;
}
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
SysUser user = userService.getByAccount(userToken.getUsername(), "1");
if (user == null) {
throw new UnknownAccountException("用户不存在!");
}
if (!"1".equals(user.getAae100())) {
throw new LockedAccountException("账户已锁定!");
}
return new SimpleAuthenticationInfo(user, user.getPassword(),ByteSource.Util.bytes(user.getAccount()), this.getName());
}
4、写个测试类,生成一个密码,账户的密码test,盐值test(一般为用户id或账户),迭代次数1024
public static void main(String[] args) {
Object obj = new SimpleHash("MD5", "test", ByteSource.Util.bytes("test"), 1024);
System.out.println(obj);
}
5、登录测试,可以跟踪到SimpleCredentialsMatcher的equals方法,查看盐值是否正确。
redis管理session
1、pom添加依赖,使用shiro-redis
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
<exclusions>
<exclusion>
<artifactId>shiro-core</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
</exclusions>
</dependency>
2、shiroconfig中设置一个redisManager(连接redis的ip端口及db等),设置sessionDAO用于redis保存的前缀与过期时间等,将session委托给redis进行管理。CustomWebSessionManager从请求的header中获取token。
@Bean
public SecurityManager securityManager(@Qualifier("customAuthorizingRealm") CustomAuthorizingRealm customAuthorizingRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
// securityManager.setCacheManager(cacheManager());
securityManager.setRealm(customAuthorizingRealm);
return securityManager;
}
@Bean
public SessionManager sessionManager() {
CustomWebSessionManager mySessionManager = new CustomWebSessionManager();
mySessionManager.setSessionDAO(redisSessionDAO());
return mySessionManager;
}
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setExpire(1800);
redisSessionDAO.setKeyPrefix("kq:session:");
return redisSessionDAO;
}
private RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost + ":" + redisPort);
redisManager.setDatabase(redisDataBase);
return redisManager;
}
url过滤
1、获取当前需要鉴权的uri,CustomPermissionsAuthorizationFilter,由于我们使用了菜单方式鉴权,不是每个url均需要鉴权的形式,所以修改了此filter,比如/web/teacher/page/pagerno,我们只鉴权/teacher这个路径在sys_fun中是否存在。
private String[] buildPermissions(ServletRequest request) {
String[] perms = new String[1];
HttpServletRequest req = (HttpServletRequest) request;
String path = req.getServletPath();
String temp = path.replace(BaseUriHead.WEB + SLASH, "");
if (temp.indexOf(SLASH) != -1) {
perms[0] = SLASH + temp.substring(0, temp.indexOf(SLASH));
} else {
perms[0] = SLASH + temp;
}
return perms;
}
2、debug会发现走到了CustomAuthorizingRealm
自定义加密
1、 自定义密码校验规则,本代码使用Aes加密
public class CredentialMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
String password = new String(userToken.getPassword());
String dbPassword = (String) info.getCredentials();
return this.equals(AesUtils.encrypt(password), dbPassword);
}
}
2、在shiroConfig中配置bean
@Bean("credentialMatcher")
public CredentialMatcher credentialMatcher() {
return new CredentialMatcher();
}
@Bean
public CustomAuthorizingRealm customAuthorizingRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {
CustomAuthorizingRealm myShiroRealm = new CustomAuthorizingRealm();
myShiroRealm.setCredentialsMatcher(matcher);
return myShiroRealm;
}
3、在Realm认证中配置
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (authenticationToken.getPrincipal() == null) {
return null;
}
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
SysUser user = userService.getByAccount(userToken.getUsername(), "1");
if (user == null) {
throw new UnknownAccountException("用户不存在!");
}
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
}
前后端分离问题
1、有OPTIONS预请求,来进行确认请求是否可用。需要过滤器返回true
CustomPermissionsAuthorizationFilter的preHandle方法
2、无权限会返回自定义的unauth页面,302无法再次进行请求导致无权限访问的401没办法显示
CustomPermissionsAuthorizationFilter,onAccessDenied中直接返回401,不redirect
3、对于超时,可以判断当前session是否新建如果新建为403,CustomPermissionsAuthorizationFilter的onAccessDenied方法
以下为源码【自定义加密方式】
- 首先进行shiro配置
@Configuration
public class ShiroConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
@Value("${spring.redis.database}")
private Integer redisDataBase;
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(1024);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
@Bean("customAuthorizingRealm")
public CustomAuthorizingRealm customAuthorizingRealm() {
CustomAuthorizingRealm customAuthorizingRealm = new CustomAuthorizingRealm();
customAuthorizingRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return customAuthorizingRealm;
}
@Bean
public JavaUuidSessionIdGenerator sessionIdGenerator() {
return new JavaUuidSessionIdGenerator();
}
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new HashMap<>(4);
filterMap.put("authc", new CustomPermissionsAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filterMap);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
/*
* anon:所有url都都可以匿名访问
* authc:所有url都必须认证通过才可以访问
*/
filterChainDefinitionMap.put("/web/login", "anon");
filterChainDefinitionMap.put("/web/unauth", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger-resources", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/web/**", "authc");
filterChainDefinitionMap.put("/web/login/logout", "logout");
shiroFilterFactoryBean.setLoginUrl("/web/unauth");
shiroFilterFactoryBean.setUnauthorizedUrl("web/unauth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(@Qualifier("customAuthorizingRealm") CustomAuthorizingRealm customAuthorizingRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
// securityManager.setCacheManager(cacheManager());
securityManager.setRealm(customAuthorizingRealm);
return securityManager;
}
@Bean
public SessionManager sessionManager() {
CustomWebSessionManager mySessionManager = new CustomWebSessionManager();
mySessionManager.setSessionDAO(redisSessionDAO());
return mySessionManager;
}
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setExpire(1800);
redisSessionDAO.setKeyPrefix("kq:session:");
return redisSessionDAO;
}
private RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost + ":" + redisPort);
redisManager.setDatabase(redisDataBase);
return redisManager;
}
@Bean("redisCacheManager")
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setPrincipalIdFieldName("userid");
return redisCacheManager;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
- 自定义session管理
public class CustomWebSessionManager extends DefaultWebSessionManager {
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
String token = httpServletRequest.getHeader("token");
if (StringUtils.isNotBlank(token)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "token");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return token;
} else {
return super.getSessionId(request, response);
}
}
/**
* //重写这个方法为了减少多次从redis中读取session(自定义redisSessionDao中的doReadSession方法)
* @param sessionKey
* @return
*/
@Override
protected Session retrieveSession(SessionKey sessionKey) {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
if (request != null && sessionId != null) {
Session session = (Session) request.getAttribute(sessionId.toString());
if (session != null) {
return session;
}
}
Session session = super.retrieveSession(sessionKey);
if (request != null && sessionId != null) {
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}
- 鉴权+认证
@Slf4j
public class CustomAuthorizingRealm extends AuthorizingRealm {
@Resource
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUser user = (SysUser) principals.getPrimaryPrincipal();
for (SysRole role : user.getRoles()) {
authorizationInfo.addRole(role.getRolename());
for (SysFun fun : role.getFuns()) {
if ("3".equals(fun.getFuntype())) {
authorizationInfo.addStringPermission(fun.getFunurl());
}
}
}
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (authenticationToken.getPrincipal() == null) {
return null;
}
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
SysUser user = userService.getByAccount(userToken.getUsername(), "1");
if (user == null) {
throw new UnknownAccountException("用户不存在!");
}
if (!"1".equals(user.getAae100())) {
throw new LockedAccountException("账户已锁定!");
}
return new SimpleAuthenticationInfo(user, user.getPassword(),ByteSource.Util.bytes(user.getAccount()), this.getName());
}
}
- 获取需要鉴权的uri
public class CustomPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {
private static final String SLASH = "/";
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return super.preHandle(request, response);
}
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
return super.isAccessAllowed(request, response, buildPermissions(request));
}
private String[] buildPermissions(ServletRequest request) {
String[] perms = new String[1];
HttpServletRequest req = (HttpServletRequest) request;
String path = req.getServletPath();
String temp = path.replace(BaseUriHead.WEB + SLASH, "");
if (temp.indexOf(SLASH) != -1) {
perms[0] = SLASH + temp.substring(0, temp.indexOf(SLASH));
} else {
perms[0] = SLASH + temp;
}
return perms;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
if(request instanceof ShiroHttpServletRequest){
ShiroHttpServletRequest req = (ShiroHttpServletRequest) request;
HttpSession session = req.getSession();
if(session.isNew()){
WebUtils.toHttp(response).sendError(403);
return false;
}
}
WebUtils.toHttp(response).sendError(401);
return false;
}
}