前后端分离、跨域、记住密码、session超时
『Shiro』可以非常快速的完成认证、授权等功能的开发,降低系统成本。实现用户身份认证,权限授权、加密、会话管理等功能。
此『Demo』为『Maven』工程,前后端分离。前端略显基础,着重后端,采用技术为:Spring Boot、MyBatis、MySQL、Shiro
。
『pom.xml』配置如下:
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xxyl</groupId>
<artifactId>cpr_manager</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cpr_manager</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!--实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<!-- 分页 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
<!--validation-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
开发用户--角色--权限管理系统,一般会涉及五张基础表:
建表语句如下:
create table users (
id int not null auto_increment,
name varchar(100) commit '用户名',
password varchar(100) commit '密码',
salt varchar(100) commit '盐'
) charset=utf8 ENGINE=InnoDB;
create table roles (
id int not null auto_increment,
name varchar(100) commit '角色名',
desc_ varchar(255) commit '角色描述'
) charset=utf8 ENGINE=InnoDB;
create table permission (
id int not null auto_increment,
name varchar(100) commit '权限名',
desc_ varchar(255) commit '权限描述'
) charset=utf8 ENGINE=InnoDB;
create table users_roles (
id int not null auto_increment,
uid int commit '用户id',
rid int commit '角色id'
) charset=utf8 ENGINE=InnoDB;
create table roles_permission (
id int not null auto_increment,
rid int commit '角色id',
pid int commit '权限id'
) charset=utf8 ENGINE=InnoDB;
@Configuration
public class ShiroConfig {
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 没有登录或登陆失效的用户请求页面时候自动跳转到登录页
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录的用户访问没有授权的资源自动跳转的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
Map<String, Filter> filterMap = new LinkedHashMap<String, Filter>();
// 解决跨域、过滤options请求问题。
filterMap.put("url", new SimpleCORSFilter());
//拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//配置映射关系 登录页和无权限页无需认证
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/notRole", "anon");
// 此处可以直接配置退出,如果需要在退出时候做一些操作,则自写接口。
// filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/**", "url");
shiroFilterFactoryBean.setFilters(filterMap);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置realm.
securityManager.setRealm(getDatabaseRealm());
// 使用自定义的会话管理
securityManager.setSessionManager(sessionManager());
// 记住密码, 7天
securityManager.setRememberMeManager(cookieRememberMeManager());
return securityManager;
}
@Bean("sessionManager")
public SessionManager sessionManager() {
SessionManager manager = new SessionManager();
/*使用了shiro自带缓存,
如果设置 redis为缓存需要重写CacheManager(其中需要重写Cache)
manager.setCacheManager(this.RedisCacheManager());*/
manager.setSessionDAO(new EnterpriseCacheSessionDAO());
return manager;
}
@Bean
public CustomRealm getDatabaseRealm() {
CustomRealm myShiroRealm = new CustomRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码;
* )
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* *
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* *
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
* * @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 1.配置Cookie对象
* 记住我的cookie:rememberMe
* @return SimpleCookie rememberMeCookie
*/
@Bean
public SimpleCookie rememberMeCookie() {
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//simpleCookie.setHttpOnly(true);
//单位(秒)7天
simpleCookie.setMaxAge(60*60*24*7);
return simpleCookie;
}
/**
* 2.配置cookie管理对象
* @return CookieRememberMeManager
*/
@Bean
public CookieRememberMeManager cookieRememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
return cookieRememberMeManager;
}
}
@Component
@WebFilter(filterName = "SimpleCORSFilter", urlPatterns ="/*" )
public class SimpleCORSFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(SimpleCORSFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
logger.info("请求拦截:"+ ((HttpServletRequest) request).getRequestURI());
/*
* 设置允许跨域请求
*/
httpServletResponse.setHeader("Access-Control-Allow-Headers", "content-type, accept");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST");
httpServletResponse.setStatus(200);
httpServletResponse.setContentType("text/plain;charset=utf-8");
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
httpServletResponse.setHeader("Access-Control-Max-Age", "0");
httpServletResponse.setHeader("Access-Control-Allow-Headers",
"Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, " +
"Content-Type, X-E4M-With,userId,token,WG-Token, Authorization");
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("XDomainRequestAllowed", "1");
/*
过虑 OPTIONS 请求
*/
String type = httpServletRequest.getMethod();
if (type.toUpperCase().equals("OPTIONS")) {
return;
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// String isCrossStr = filterConfig.getInitParameter("IsCross");
// isCross = isCrossStr.equals("true") ? true : false;
// System.out.println(isCrossStr);
}
@Override
public void destroy() {
// isCross = false;
}
}
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserBiz userBiz;
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//1. 从 PrincipalCollection 中来获取登录用户的信息
String userName = (String) principalCollection.getPrimaryPrincipal();
User user = userBiz.findUserByUsername(userName);
//2.添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
Set<String> permissions = new HashSet<>();
Set<String> roles = new HashSet<>();
for (Role role : user.getRoles()) {
//2.1添加角色
roles.add(role.getName());
for (Permission permission : role.getPermissions()) {
//2.1.1添加权限
permissions.add(permission.getName());
}
}
simpleAuthorizationInfo.addRoles(roles);
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String userName = (String) authenticationToken.getPrincipal();
//同一账号同一时间只能在一个地方登陆
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
DefaultWebSessionManager sessionManager = (DefaultWebSessionManager)securityManager.getSessionManager();
Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();//获取当前已登录的用户session列表
for(Session session:sessions){
//清除该用户以前登录时保存的session
if(userName.equals(String.valueOf(session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY)))) {
sessionManager.getSessionDAO().delete(session);
}
}
//根据用户名从数据库获取密码
User user = userBiz.findUserByUsername(userName);
String passwordInDB = "", salt = "";
if (user != null) {
passwordInDB = user.getPassword();
salt = user.getSalt();
}
return new SimpleAuthenticationInfo(userName, passwordInDB, ByteSource.Util.bytes(salt), getName());
}
}
public class SessionManager extends DefaultWebSessionManager {
private static final Logger log = LoggerFactory.getLogger(DefaultWebSessionManager.class);
private String authorization = "Authorization";
/**
* 重写获取sessionId的方法调用当前Manager的获取方法
*
* @param request
* @param response
* @return
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
return this.getReferencedSessionId(request, response);
}
/**
* 获取sessionId从请求中
*
* @param request
* @param response
* @return
*/
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
String id = this.getSessionIdCookieValue(request, response);
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie");
} else {
id = this.getUriPathSegmentParamValue(request, "JSESSIONID");
if (id == null) {
// 获取请求头中的session
id = WebUtils.toHttp(request).getHeader(this.authorization);
if (id == null) {
String name = this.getSessionIdName();
id = request.getParameter(name);
if (id == null) {
id = request.getParameter(name.toLowerCase());
}
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "url");
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
return id;
}
private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
if (!this.isSessionIdCookieEnabled()) {
log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
return null;
} else if (!(request instanceof HttpServletRequest)) {
log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie. Returning null.");
return null;
} else {
HttpServletRequest httpRequest = (HttpServletRequest) request;
return this.getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
}
}
private String getUriPathSegmentParamValue(ServletRequest servletRequest, String paramName) {
if (!(servletRequest instanceof HttpServletRequest)) {
return null;
} else {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String uri = request.getRequestURI();
if (uri == null) {
return null;
} else {
int queryStartIndex = uri.indexOf(63);
if (queryStartIndex >= 0) {
uri = uri.substring(0, queryStartIndex);
}
int index = uri.indexOf(59);
if (index < 0) {
return null;
} else {
String TOKEN = paramName + "=";
uri = uri.substring(index + 1);
index = uri.lastIndexOf(TOKEN);
if (index < 0) {
return null;
} else {
uri = uri.substring(index + TOKEN.length());
index = uri.indexOf(59);
if (index >= 0) {
uri = uri.substring(0, index);
}
return uri;
}
}
}
}
}
private String getSessionIdName() {
String name = this.getSessionIdCookie() != null ? this.getSessionIdCookie().getName() : null;
if (name == null) {
name = "JSESSIONID";
}
return name;
}
}
@Controller
public class LoginController {
@Autowired
public UserBiz userBiz;
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public JSONObject login(@Validated LoginDTO loginDTO) {
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(loginDTO.getUsername(), loginDTO.getPassword());
JSONObject jsonObject = new JSONObject();
// 执行认证登陆
try {
// 无操作30分钟后登录失效。
subject.getSession().setTimeout(1800000);
subject.login(token);
jsonObject.put("username", user.getNickname());
} catch (AuthenticationException ae) {
jsonObject.put("msg", "用户名或密码不正确");
return jsonObject;
}
if (subject.isAuthenticated()) {
jsonObject.put("sessionId",subject.getSession().getId());
Session session = subject.getSession();
session.setAttribute("subject", subject);
return jsonObject;
} else {
token.clear();
jsonObject.put("msg", "登录失败");
return jsonObject;
}
}
@ResponseBody
@RequestMapping(value = "/logout")
public JSONObject logout() {
try {
Subject lvSubject=SecurityUtils.getSubject();
lvSubject.logout();
// 如果退出时候有其他操作,可以写在此处
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg", "退出成功");
return jsonObject;
} catch (Exception e){
jsonObject.put("msg", "退出失败");
return jsonObject;
}
}
}
@RequiresPermissions("addUser") // 使用注解方式来验证权限
@ResponseBody
@RequestMapping(value = "/addUser")
public JSONObject addUser(User user, @RequestParam String role){
try {
JSONObject jsonObject = new JSONObject();
// 验证输入
... ...
// 生成密码以及盐
PasswordAndSalt passwordAndSalt= new PasswordAndSalt();
String[] strings= passwordAndSalt.getPasswordAndSalt("", 2);
user.setPassword(strings[1]);
user.setSalt(strings[0]);
Date d = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
user.setCreatetime(sdf.format(d));
userMapper.addUser(user);
int id= user.getId(); // 插入后的id
String[] strings1= role.split(";");
List<UserRole> list= new ArrayList<>();
for (String str : strings1) {
UserRole userRole= new UserRole();
userRole.setUid(id);
userRole.setRid(Integer.parseInt(str));
list.add(userRole);
}
courseMapper.addUserRole(list); // 插入用户-角色关系
}catch (Exception e){
e.printStackTrace();
jsonObject.put("msg", "账号添加失败");
return jsonObject;
}
jsonObject.put("msg", "账号添加成功");
return jsonObject;
}
PasswordAndSalt.java
public class PasswordAndSalt {
public static String password = "123456";
public static int number= 2; // 加密次数
public String[] getPasswordAndSalt(String pwd, int num){
if(pwd.equals("")){
pwd= password;
}
if(num<= 0){
num= number;
}
String newSalt = new SecureRandomNumberGenerator().nextBytes().toString(); //盐量随机
String newPwd= new Md5Hash(pwd, newSalt, num).toString(); // 新密码
String[] strings= new String[2];
strings[0]= newSalt;
strings[1]= newPwd;
return strings;
}
}
@RequiresPermissions("changePassword")
@ResponseBody
@RequestMapping("/changePassword")
public JSONObject showUser(String old, String pwd1, String pwd2) {
try {
JSONObject jsonObject = new JSONObject();
String loginAccount = SecurityUtils.getSubject().getPrincipal().toString();
if(!pwd1.equals(pwd2)){
jsonObject.put("msg", "密码不一致,请重新输入");
return jsonObject;
}
//根据用户名从数据库获取密码
User user = userBiz.findUserByUsername(loginAccount);
String passwordInDB = "", salt = "";
if (user != null) {
passwordInDB = user.getPassword();
salt = user.getSalt();
}else {
jsonObject.put("msg", "用户不存在");
return jsonObject;
}
String str= new Md5Hash(old, salt, 2).toString();
if(!str.equals(passwordInDB)){
jsonObject.put("msg", "请输入正确的原始密码");
return jsonObject;
}
String[] strings= passwordAndSalt.getPasswordAndSalt(pwd1, 2);
String newSalt = strings[0]; //盐量随机
String newPwd= strings[1]; // 新密码
Boolean flag= userBiz.changePassword(newPwd, newSalt, user.getId());
jsonObject.put("msg", "密码修改成功");
return jsonObject;
}catch (Exception e){
e.printStackTrace();
jsonObject.put("msg", "密码修改失败");
return jsonObject;
}
}
注解验证角色和权限的话无法捕捉异常,从而无法正确的返回给前端错误信息,所以加了一个类用于拦截异常,具体代码如下:
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(value = AuthorizationException.class)
public Map<String, String> handleException(AuthorizationException e) {
Map<String, String> result = new HashMap<String, String>();
result.put("status", "400");
//获取错误中中括号的内容
String message = e.getMessage();
String msg= null;
try {
msg = message.substring(message.indexOf("[")+1,message.indexOf("]"));
}catch (StringIndexOutOfBoundsException e1){
result.put("msg", "对不起,您没有权限,请先登录");
return result;
}
//判断是角色错误还是权限错误
if (message.contains("role")) {
result.put("msg", "对不起,您没有" + msg + "角色");
} else if (message.contains("permission")) {
result.put("msg", "对不起,您没有" + msg + "权限");
} else {
result.put("msg", "对不起,您的权限有误");
}
return result;
}
}
本文由 caroly 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载 / 出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://caroly.fun/archives/springbootshiro权限控制
最后更新:2021-04-29 14:47:03
Update your browser to view this website correctly. Update my browser now