任务描述
本关任务:编写登录接口完成用户登录。
相关知识
为了完成本关任务,你需要了解:1.安全框架Apache Shiro,2.如何配置Shiro框架,3,如何编写登录接口。
安全框架Apache Shiro
Apache Shiro 是一个功能强大且易于使用的 Java 安全框架,它为开发人员提供了一个直观而全面的身份验证、授权、加密和会话管理解决方案。不仅可以用在JavaSE环境,也可以用在JavaEE环境,可以完成:认证、授权、加密、会话管理、与Web集成、缓存等。如下是它具有的特点:
1,易于理解的Java Security API;
2,简单的身份认证(登录),支持多种数据源(LDAP、JDBC、Kerberos, ActiveDirectory等);
3,对角色的简单的签权(访问控制),支持细粒度的签权;
4,支持一级缓存,以提升应用程序的性能;
5,内置的基于POJO企业会话管理,适用于Web以及非Web的环境;
6,异构客户端会话访问;
7,非常简单的加密API;
8,不跟任何框架或者容器捆绑,可以独立运行。
Spring Security与Shiro的不同点:
1:Spring Security基于Spring开发,项目中如果使用Spring作为基础,配合Spring Security做权限更加方便,而Shiro需要和Spring进行整合开发
2:Spring Security功能比Shiro更加丰富些,例如安全防护
3:Spring Security社区资源比Shiro丰富。
但是Shiro的配置和使用比较简单,依赖性低,Spring Security上手复杂,所以本项目使用Shiro框架。
Shiro架构
Shiro架构分为三个部分,如下:
Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外
API 核心就是 Subject。Subject 代表了当前“用户”, 这个用户不一定
是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,
机器人等;与 Subject 的所有交互都会委托给 SecurityManager;
Subject 其实是一个门面,SecurityManager 才是实际的执行者;
SecurityManager:安全管理器;即所有与安全有关的操作都会与
SecurityManager 交互;且其管理着所有 Subject;可以看出它是 Shiro
的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中
DispatcherServlet 的角色。
Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说
SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户
进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/
权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource。
如何配置Shiro框架
首先添加maven依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
新建一个角色类,主要包括角色id、角色名称、角色代码。
package net.educoder.ims.pojo;
import java.io.Serializable;
/**
* 角色实体类
* @author admin
* @date 2021-07-19
*/
public class Role implements Serializable {
/**
* 角色表id
*/
private Integer rid;
/**
* 角色名称
*/
private String rname;
/**
* 角色代码
*/
private String rcode;
private static final long serialVersionUID = 1L;
public Integer getRid() {
return rid;
}
public void setRid(Integer rid) {
this.rid = rid;
}
public String getRname() {
return rname;
}
public void setRname(String rname) {
this.rname = rname == null ? null : rname.trim();
}
public String getRcode() {
return rcode;
}
public void setRcode(String rcode) {
this.rcode = rcode == null ? null : rcode.trim();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", rid=").append(rid);
sb.append(", rname=").append(rname);
sb.append(", rcode=").append(rcode);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
Role other = (Role) that;
return (this.getRid() == null ? other.getRid() == null : this.getRid().equals(other.getRid()))
&& (this.getRname() == null ? other.getRname() == null : this.getRname().equals(other.getRname()))
&& (this.getRcode() == null ? other.getRcode() == null : this.getRcode().equals(other.getRcode()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getRid() == null) ? 0 : getRid().hashCode());
result = prime * result + ((getRname() == null) ? 0 : getRname().hashCode());
result = prime * result + ((getRcode() == null) ? 0 : getRcode().hashCode());
return result;
}
}
新建一个权限实体类,包括权限名称以及资源标识,代码类似。
新建一个UserRealm继承自AuthorizingRealm,实现我们的认证和授权逻辑。由于我们使用了state标识用户身份,而且我们需要根据不同的身份去不同的表查询相应的数据库,所以我们自定义了UsernamePasswordToken继承org.apache.shiro.authc.UsernamePasswordToken,在其中加入了state字段。
/**
* @author Jason
* @create 2021-07-19 9:11
*/
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
/**
* 清除缓存
*/
public void clearCache() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
int state = usernamePasswordToken.getState();
if (state == StateEnums.ADMIN.getCode()) {
Admin admin = userService.getAdminByUsername(username);
if (admin == null || admin.getAdmin() == 0) {
throw new ImsException(ResultEnum.ERROR.getCode(), "账号异常!");
}
return new SimpleAuthenticationInfo(admin, admin.getPassword(), this.getName());
} else {
if (state == 1) {
Student stu = userService.getStuByUsername(username);
if (stu == null || stu.getStatus() == -1) {
throw new ImsException(ResultEnum.ERROR.getCode(), "账号异常!");
}
return new SimpleAuthenticationInfo(stu, stu.getPassword(), this.getName());
}
Teacher teacher = userService.getTeacherByUsername(username);
if (teacher == null || teacher.getStatus() == -1) {
throw new ImsException(ResultEnum.ERROR.getCode(), "账号异常!");
}
return new SimpleAuthenticationInfo(teacher, teacher.getPassword(), this.getName());
}
}
}
首先要配置的是ShiroConfig类,Apache Shiro 核心通过 Filter 来实现,就好像 SpringMVC 通过 DispachServlet 来主控制一样。既然是使用 Filter 一般也就能猜到,是通过 URL 规则来进行过滤和权限校验,所以我们需要定义一系列关于 URL 的规则和访问权限。
package net.educoder.ims.config;
import com.google.common.collect.Maps;
import net.educoder.ims.filters.LoginFilter;
import net.educoder.ims.realm.UserRealm;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.Map;
/**
* shiro配置类
* @author admin
*/
@Configuration
public class ShiroConfig {
/**
* 创建ShiroFilterFactoryBean
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
// 设置安全管理器
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 把自定义的过滤器放入shiro中
Map<String, Filter> shiroFilters = shiroFilterFactoryBean.getFilters();
/**
* 常用过滤器
* anon:无需认证可以访问
* authc:必须认证才能访问
* user:如果使用rememberMe的功能可以直接访问
* perms:该资源必须得到权限才可以访问
* role:该资源必须得到角色权限才可以访问
*/
Map<String, String> filterMap = Maps.newHashMap();
filterMap.put("/user/login", "anon");
filterMap.put("/user/logout", "logout");
filterMap.put("/*/register", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/druid", "anon");
filterMap.put("/*/*/springfox.js", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/webjars/springfox-swagger-ui/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/actuator/**", "roles[admin]");
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
@Bean(name="sessionManager")
public DefaultWebSessionManager defaultWebSessionManager() {
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
shiroSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
return shiroSessionManager;
}
/**
* 创建DefaultSecurityManager
*/
@Bean
public SecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联realm
securityManager.setRealm(userRealm);
securityManager.setSessionManager(defaultWebSessionManager());
securityManager.setCacheManager(ehCacheManager());
return securityManager;
}
/**
* 创建Realm
*/
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCachingEnabled(true);
// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
userRealm.setAuthenticationCachingEnabled(true);
// 缓存AuthenticationInfo信息的缓存名称 在 ehcache-shiro.xml 中有对应缓存的配置
userRealm.setAuthenticationCacheName("authenticationCache");
// 启用授权缓存,即缓存AuthorizationInfo信息,默认false
userRealm.setAuthorizationCachingEnabled(true);
// 缓存AuthorizationInfo 信息的缓存名称 在 ehcache-shiro.xml 中有对应缓存的配置
userRealm.setAuthorizationCacheName("authorizationCache");
return userRealm;
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启shiro aop注解支持.
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* shiro缓存管理器;
* 需要添加到securityManager中
* @return
*/
@Bean
public EhCacheManager ehCacheManager(){
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return cacheManager;
}
}
为了防止用户未经登录就访问其它页面以及跨域等问题,我们还需要自定义登录拦截器:
package net.educoder.ims.filters;
import com.alibaba.fastjson.JSONObject;
import net.educoder.ims.enums.ResultEnum;
import net.educoder.ims.utils.Result;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录拦截器
* @author admin
*/
public class LoginFilter extends UserFilter {
/**
* 这个方法用于处理未登录时页面重定向的逻辑
* 因此,只要进入了这个方法,就意味着登录失效了
* 我们只需要在这个方法里,给前端返回一个登录失效的状态码即可
* @param request
* @param response
* @throws IOException
*/
@Override
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSONObject.toJSONString(new Result<>(ResultEnum.NOT_LOGIN)));
}
/**
* 解决复杂请求跨域 如预处理跨域问题
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpRequest = WebUtils.toHttp(request);
HttpServletResponse httpResponse = WebUtils.toHttp(response);
if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpResponse.setHeader("Access-control-Allow-Origin", httpRequest.getHeader("Origin"));
httpResponse.setHeader("Access-Control-Allow-Methods", httpRequest.getMethod());
httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
httpResponse.setStatus(HttpStatus.OK.value());
return true;
}
return super.preHandle(request, response);
}
/**
* 解决OPTIONS请求被拦截问题
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String method = "OPTIONS";
if (request instanceof HttpServletRequest) {
if (method.equals(((HttpServletRequest) request).getMethod().toUpperCase())) {
HttpServletResponse httpResp = WebUtils.toHttp(response);
httpResp.setStatus(HttpStatus.OK.value());
return true;
}
}
return super.isAccessAllowed(request, response, mappedValue);
}
}
然后需要在ShiroConfig.java的ShiroFilterFactoryBean中放置自定义的过滤器:
// 把自定义的过滤器放入shiro中
Map<String, Filter> shiroFilters = shiroFilterFactoryBean.getFilters();
shiroFilters.put("authc", new LoginFilter());
最后,我们自定义session获取方式,如果在Cookie中存在的话就取出sessionId,否则去请求头中寻找Authorization。
package net.educoder.ims.config;
import net.educoder.ims.utils.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
import java.util.Enumeration;
/**
* 自定义session获取方式
* 采用ajax请求头authToken携带sessionId的方式
* @author Jason
* @create 2021-08-06 15:44
*/
public class ShiroSessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public ShiroSessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
Enumeration<String> headers = WebUtils.toHttp(request).getHeaderNames();
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
if (StringUtils.isEmpty(id)) {
//如果没有携带id参数则按照父类的方式在cookie进行获取
return super.getSessionId(request, response);
} else {
//如果请求头中有 Authorization 则其值为sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
}
}
如何编写登录接口
在这里我们遵循自定义的规则,用户名长度为12的用户代表学生,管理名称前缀都有admin,首先我们在controller层中编写一个/login的post接口,并判断传入的参数是否为空:
@PostMapping(value = "/login")
public Result<Object> login(@RequestBody UserVO user) {
// 用户名长度为12 表示学生
final int codeLength = 12;
// 管理员名称前缀
final String adminCode = "admin";
// 管理员前缀长度
final int adminCodeLength = 5;
UserVO userVO = new UserVO();
Set<Permission> permissionSet = new HashSet();
Set<Role> roleSet = new HashSet();
if (user == null || StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
return new Result<>(ResultEnum.PARAMS_NULL.getCode(), "用户名或密码错误!");
}
return null;
}
然后我们通过SecurityUtils工具类从线程上下文中拿到subject,然后调用subjuect.login()实现登录,并获取sessionId作为token返回前端。
/**
* 用户登录
* @param user
* @return
*/
@ApiOperation(value = "登录接口",httpMethod = "POST",notes = "用户名,密码,必填")
@ApiImplicitParams({@ApiImplicitParam(name="user",value="用户对象",dataType="UserVO",paramType = "body",required = false)})
@PostMapping(value = "/login")
public Result<Object> login(@RequestBody UserVO user) {
// 用户名长度为12 表示学生
final int codeLength = 12;
// 管理员名称前缀
final String adminCode = "admin";
// 管理员前缀长度
final int adminCodeLength = 5;
UserVO userVO = new UserVO();
Set<Permission> permissionSet = new HashSet();
Set<Role> roleSet = new HashSet();
if (user == null || StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
return new Result<>(ResultEnum.PARAMS_NULL.getCode(), "用户名或密码错误!");
}
Subject subject = SecurityUtils.getSubject();
if (user.getUsername().length() >= adminCodeLength && user.getUsername().substring(0,adminCodeLength).equals(adminCode)) {
user.setStatus(0);
} else if (user.getUsername().length() == codeLength) {
user.setStatus(1);
} else {
user.setStatus(2);
}
AuthenticationToken authenticationToken = new UsernamePasswordToken(user.getUsername(), user.getPassword(),user.getStatus());
try {
subject.login(authenticationToken);
} catch (Exception e) {
return new Result<>(ResultEnum.PARAMS_NULL.getCode(), "用户名或密码错误!");
}
// 登录成功
Serializable sessionId = subject.getSession().getId();
try {
Admin admin = (Admin) subject.getPrincipal();
userVO.setStatus(0);
BeanUtils.copyProperties(admin,userVO);
} catch (ClassCastException e) {
BeanUtils.copyProperties(subject.getPrincipal(),userVO);
}
List<String> roleList = roleService.findRoleById(userVO.getId(), userVO.getStatus());
roleList.forEach(rid -> {
roleSet.add(roleService.findById(Integer.valueOf(rid)));
permissionSet.addAll(permissionService.findPermissionsByRid(Integer.valueOf(rid)));
});
userVO.setPassword("");
Map<String, Object> returnMap = new HashMap<>(2);
returnMap.put("token", sessionId);
returnMap.put("user", userVO);
returnMap.put("roleList", roleSet);
returnMap.put("permissionList", permissionSet);
return new Result<>(returnMap);
}
编程要求
请仔细阅读右侧代码,结合相关知识,让我们编写一个用户登录接口,在Begin-End区域内进行代码补充:
完成ShiroConfig相关代码;
在UserController增加前端请求接口,设置请求地址为/login。
测试说明
平台会对你的代码进行运行测试,如果实际输出与预期输出只有header不一致,则算通关。另外,由于本关项目文件较大,评测时间较长,请同学们耐心等待!package net.educoder.ims.controller;
import net.educoder.ims.enums.ResultEnum;
import net.educoder.ims.enums.StateEnums;
import net.educoder.ims.exception.ImsException;
import net.educoder.ims.pojo.*;
import net.educoder.ims.service.PermissionService;
import net.educoder.ims.service.RoleService;
import net.educoder.ims.service.UserService;
import net.educoder.ims.token.UsernamePasswordToken;
import net.educoder.ims.utils.IdWorker;
import net.educoder.ims.utils.Page;
import net.educoder.ims.utils.Result;
import net.educoder.ims.utils.StringUtils;
import net.educoder.ims.vo.UserVO;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.Serializable;
import java.util.*;
/**
* 用户公用接口
* @author Jason
* @create 2021-07-19 9:55
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
/**
* 用户登录
* @param user
* @return
*/
/****************** Begin ******************/
@PostMapping(value = "/login")
public Result<Object> login(@RequestBody UserVO user) {
// 用户名长度为12 表示学生
final int codeLength = 12;
// 管理员名称前缀
final String adminCode = "admin";
// 管理员前缀长度
final int adminCodeLength = 5;
UserVO userVO = new UserVO();
Set<Permission> permissionSet = new HashSet();
Set<Role> roleSet = new HashSet();
if (user == null || StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
return new Result<>(ResultEnum.PARAMS_NULL.getCode(), "用户名或密码错误!");
}
Subject subject = SecurityUtils.getSubject();
// 补充代码,根据名称长度判断用户身份
// 补充代码,使用自定义的UsernamePasswordToken生成token
AuthenticationToken authenticationToken = new UsernamePasswordToken(user.getUsername(), user.getPassword(),user.getStatus());
try {
// 补充代码,调用subject.login并传入AuthenticationToken
} catch (Exception e) {
return new Result<>(ResultEnum.PARAMS_NULL.getCode(), "用户名或密码错误!");
}
// 登录成功
Serializable sessionId = subject.getSession().getId();
try {
Admin admin = (Admin) subject.getPrincipal();
userVO.setStatus(0);
BeanUtils.copyProperties(admin,userVO);
} catch (ClassCastException e) {
BeanUtils.copyProperties(subject.getPrincipal(),userVO);
}
List<String> roleList = roleService.findRoleById(userVO.getId(), userVO.getStatus());
roleList.forEach(rid -> {
roleSet.add(roleService.findById(Integer.valueOf(rid)));
permissionSet.addAll(permissionService.findPermissionsByRid(Integer.valueOf(rid)));
});
userVO.setPassword("");
Map<String, Object> returnMap = new HashMap<>(2);
returnMap.put("token", sessionId);
returnMap.put("user", userVO);
returnMap.put("roleList", roleSet);
returnMap.put("permissionList", permissionSet);
return new Result<>(returnMap);
}
/****************** End ******************/
}
最新发布