一、认证流程
- 账号密码登录,后端使用shiro认证
- 根据账号密码生成jwt,并放入请求头后传给前端,前端将jwt存入localStorage
- 登录成功后的操作通过JWTRealm来进行认证,如果jwt出错则无法操作
二、代码实现
2.1 项目结构
2.2 导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.1</version>
</dependency>
2.3 新建Realm,根据目前的场景,需要两个Realm
2.3.1 DbRealm
与数据库交互,认证信息直接使用UsernamePasswordToken
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.woniuxy.ticket.auth.entity.User;
import com.woniuxy.ticket.auth.mapper.UserMapper;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
/***
*自定义Realm,用于与数据库交互
*/
public class DbRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("account", upToken.getUsername());
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new UnknownAccountException("账号:" + upToken.getUsername() + "不存在");
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
//交给合适的Realm认证
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
}
2.3.2 JwtRealm
直接校验JWT是否合法,认证信息直接在JWT中,所以先自定义Token接收该JWT
JwtToken
import org.apache.shiro.authc.AuthenticationToken;
/**
* 自定义Token,用于封装JWT
*/
public class JwtToken implements AuthenticationToken {
private String jwt;
public JwtToken(String jwt) {
this.jwt = jwt;
}
@Override
public Object getPrincipal() {
return jwt;
}
@Override
public Object getCredentials() {
return jwt;
}
}
JWTRealm
import com.woniuxy.ticket.auth.jwt.Audience;
import com.woniuxy.ticket.auth.jwt.JwtUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
public class JwtRealm extends AuthorizingRealm {
@Autowired
private Audience audience;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) token;
String jwt = jwtToken.getPrincipal().toString();
if (!JwtUtil.parseJwt(jwt, audience.getBase64Secret())) {
throw new AuthenticationException("校验JWT异常");
}
return new SimpleAuthenticationInfo(jwt, jwt, getName());
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
}
2.4 自定义过滤器,用于对请求进行认证校验
JwtFilter
import com.fasterxml.jackson.databind.ObjectMapper;
import com.woniuxy.ticket.commons.entity.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义过滤器,用于JWT认证的校验
*
*/
@Slf4j
public class JwtFilter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
String jwtToken = ((HttpServletRequest) request).getHeader("jwt");
return new JwtToken(jwtToken);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (this.isLoginRequest(request, response))
return true;
boolean allowed = false;
try {
allowed = executeLogin(request, response);
} catch (IllegalStateException e) { // not found any token
log.error("Not found any token");
} catch (Exception e) {
log.error("Error occurs when login", e);
}
return allowed || super.isPermissive(mappedValue);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
ResponseResult<Void> result = new ResponseResult<>(403, "会话失效,重新登录");
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(result));
return false;
}
}
2.5 ShiroConfig
将所有的配置集中实现
@Configuration
public class ShiroConfig {
@Bean
public Realm dbRealm() {
return new DbRealm();
}
@Bean
public Realm jwtRealm() {
return new JwtRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealms(Arrays.asList(dbRealm(),jwtRealm()));
return securityManager;
}
@Bean
public JwtFilter jwtFilter() {
return new JwtFilter();
}
//核心Bean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
//增加自定义的过滤器
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//配置请求的URL与过滤器的关系
Map<String, String> map = new LinkedHashMap<>();
map.put("/manager/login", "anon");
map.put("/menu/get","jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}
2.6 在多Realm环境中,默认不会抛出特定的异常
需要新建一个类继承ModularRealmAuthenticator并重写其中doMultiRealmAuthentication方法,并且重新在ShiroConfig中的SecurityManager进行设置
2.6.1 新建MyRealmAuthenticator
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import java.util.Collection;
@Slf4j
public class MyRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
AuthenticationException authenticationException = null;
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (AuthenticationException e) {
authenticationException = e;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, e);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, authenticationException);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
//新增的代码
if (authenticationException != null) {
throw authenticationException;
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
}
2.6.2 修改ShiroConfig
import com.woniuxy.ticket.auth.shiro.*;
import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public DbRealm dbRealm() {
return new DbRealm();
}
@Bean
public Realm jwtRealm() {
return new JwtRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// securityManager.setRealm(dbRealm());
//多个Realm需要设置,不然无法捕获账号不存在和密码错误异常
//需要新建一个类继承ModularRealmAuthenticator并重写doMultiRealmAuthentication方法
MyRealmAuthenticator authenticator = new MyRealmAuthenticator();
authenticator.setRealms(Arrays.asList(dbRealm(),jwtRealm()));
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
securityManager.setAuthenticator(authenticator);
//设置授权器为DbRealm
securityManager.setAuthorizer(dbRealm());
return securityManager;
}
@Bean
public JwtFilter jwtFilter() {
return new JwtFilter();
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
//增加自定义的过滤器
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", jwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
Map<String, String> map = new LinkedHashMap<>();
map.put("/user/login", "anon");
map.put("/user/getPerms","jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}
2.7 逻辑实现
2.7.1 实体类**
UserRoleDto
@Data
public class UserRoleDto implements Serializable {
private Integer id;
private String account;
private String password;
private Integer rid;
//状态:0启用,1禁用
private Integer status;
private RolePermsDto rolePermsDto;
private static final long serialVersionUID = 1L;
}
RolePermsDto
@Data
public class RolePermsDto implements Serializable {
private Integer id;
private String name;
private List<Perms> perms;
private static final long serialVersionUID = 1L;
}
Perms
@Data
public class Perms implements Serializable {
private Integer id;
private String name;
private String code;
private String link;
private Integer parentId;
private String type;
private String status;
private static final long serialVersionUID = 1L;
}
2.7.2 UserMapper和UserMapper.xml
@Mapper
@Repository
public interface UserMapper extends BaseMapper<User> {
//通过用户id获取角色及其权限
UserRoleDto findUserPerms(@Param("id") int id);
//通过用户id获取角色及其菜单
UserRoleDto findUserMenus(@Param("id") int id);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.woniuxy.ticket.auth.mapper.UserMapper">
<!-- 查询用户权限 -->
<select id="findUserPerms" resultMap="user_role_perms_dto_map">
select id,account,rid from user where id = #{id} and status = 0
</select>
<resultMap id="user_role_perms_dto_map" type="UserRoleDto">
<id column="id" property="id"></id>
<result column="rid" property="rid"></result>
<collection property="rolePermsDto" column="rid" select="findRolePermsDtoByRid"></collection>
</resultMap>
<select id="findRolePermsDtoByRid" resultMap="role_perms_dto">
select * from role where id = #{rid}
</select>
<resultMap id="role_perms_dto" type="RolePermsDto">
<id column="id" property="id"></id>
<collection property="perms" column="id" select="findPermsByRid"></collection>
</resultMap>
<select id="findPermsByRid" resultType="Perms">
select * from role_perms rp,perms p where rp.rid = #{id} and rp.pid = p.id and p.type = 'a'
</select>
<!-- 查询用户菜单 -->
<select id="findUserMenus" resultMap="user_role_menus_dto_map">
select id,account,rid from user where id = #{id} and status = 0
</select>
<resultMap id="user_role_menus_dto_map" type="UserRoleDto">
<id column="id" property="id"></id>
<result column="rid" property="rid"></result>
<collection property="rolePermsDto" column="rid" select="findRoleMenusDtoByRid"></collection>
</resultMap>
<select id="findRoleMenusDtoByRid" resultMap="role_menus_dto">
select * from role where id = #{rid}
</select>
<resultMap id="role_menus_dto" type="RolePermsDto">
<id column="id" property="id"></id>
<collection property="perms" column="id" select="findMenusByRid"></collection>
</resultMap>
<select id="findMenusByRid" resultType="Perms">
select * from role_perms rp,perms p where rp.rid = #{id} and rp.pid = p.id and p.type = 'm'
</select>
</mapper>
2.7.3 UserService和UserServiceImpl
UserService
public interface UserService extends IService<User> {
//通过用户id获取角色及其权限
UserRoleDto findUserPerms(int id);
//通过用户id获取角色及其权限
UserRoleDto findUserMenus(int id);
}
UserServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
private UserMapper userMapper;
@Override
public UserRoleDto findUserPerms(int id) {
return userMapper.findUserPerms(id);
}
@Override
public UserRoleDto findUserMenus(int id) {
return userMapper.findUserMenus(id);
}
}
2.7.4 UserController
import com.woniuxy.ticket.auth.entity.User;
import com.woniuxy.ticket.auth.entity.dto.UserRoleDto;
import com.woniuxy.ticket.auth.jwt.Audience;
import com.woniuxy.ticket.auth.jwt.JwtUtil;
import com.woniuxy.ticket.auth.service.UserService;
import com.woniuxy.ticket.commons.entity.ResponseResult;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("user")
public class UserController {
@Resource
private Audience audience;
@Resource
private UserService userService;
@PostMapping("/login")
public ResponseResult login(User user, HttpServletResponse response) {
UsernamePasswordToken token = new UsernamePasswordToken(user.getAccount(),user.getPassword());
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
}catch (UnknownAccountException e){
return new ResponseResult(404,"账号不存在");
}catch (IncorrectCredentialsException e){
return new ResponseResult(504,"密码不正确");
}
catch (AuthenticationException e){
return new ResponseResult(505,"登录异常");
}
User user1 = (User) subject.getPrincipal();
String jwt = JwtUtil.createJWT(user1.getId(),user1.getAccount(),audience);
response.setHeader("Authorization", jwt);
return new ResponseResult().success();
}
@PostMapping("/getPerms")
public ResponseResult getPerms(@RequestHeader("jwt") String jwt){
if(JwtUtil.parseJwt(jwt,audience.getBase64Secret())){
int id = JwtUtil.getUserId(jwt,audience.getBase64Secret());
UserRoleDto userRoleDto = userService.findUserPerms(id);
return new ResponseResult().success(userRoleDto);
}
return new ResponseResult().error();
}
@PostMapping("/getMenus")
public ResponseResult getMenus(@RequestHeader("jwt") String jwt){
if(JwtUtil.parseJwt(jwt,audience.getBase64Secret())){
int id = JwtUtil.getUserId(jwt,audience.getBase64Secret());
UserRoleDto userRoleDto = userService.findUserMenus(id);
return new ResponseResult().success(userRoleDto);
}
return new ResponseResult().error();
}
}
2.7.5 前端页面
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="jquery.min.js"></script>
<script>
$(function () {
$("#loginBtn").click(function () {
let data = {
"account": $("#account").val(),
"password": $("#password").val()
}
$.post("http://localhost:9006/user/login", data, function (r, n, xhq) {
console.log(r);
if (r.statusCode === 200) {
localStorage.setItem("jwt", xhq.getResponseHeader("Authorization"));
location.href = "index.html";
} else if (r.statusCode === 404) {
alert(r.message);
} else if (r.statusCode === 504) {
alert(r.message);
} else if (r.statusCode === 505) {
alert(r.message);
}
})
})
})
</script>
</head>
<body>
<input type="text" id="account"><br>
<input type="text" id="password"><br>
<input type="button" value="登录" id="loginBtn">
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="jquery.min.js"></script>
<script>
$(function(){
$("#permsBtn").click(function(){
$.ajax({
type: "post",
url: "http://localhost:9006/user/getPerms",
headers: {'jwt':localStorage.getItem("jwt")},
success: function (r) {
console.log(r);
}
});
});
$("#menusBtn").click(function(){
$.ajax({
type: "post",
url: "http://localhost:9006/user/getMenus",
headers: {'jwt':localStorage.getItem("jwt")},
success: function (r) {
console.log(r);
}
});
});
})
</script>
</head>
<body>
首页
<button id="permsBtn">获取权限</button>
<button id="menusBtn">获取菜单</button>
</body>
</html>
2.8 测试
2.8.1 账号不存在
2.8.2 密码错误
2.8.3 正确登录后跳转
2.8.4 获取权限
2.8.5 获取菜单
2.8.6 如果不登录直接通过url访问获取权限接口
2.9 其他问题
2.9.1 跨域问题
跨域:在服务器上使用一个Filter,其中设置响应头,要将其配置到前面
TicketCrossFilter
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 设置跨域的过滤器
*/
public class TicketCrossFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String origin = req.getHeader("Origin");
if (!org.springframework.util.StringUtils.isEmpty(origin)) {
// 带cookie的时候,origin必须是全匹配,不能使用*
res.addHeader("Access-Control-Allow-Origin", origin);
}
res.addHeader("Access-Control-Allow-Methods", "*");
String headers = req.getHeader("Access-Control-Request-Headers");
// 支持所有自定义头
if (!org.springframework.util.StringUtils.isEmpty(headers)) {
res.addHeader("Access-Control-Allow-Headers", headers);
}
res.addHeader("Access-Control-Max-Age", "3600");
res.addHeader("Access-Control-Allow-Credentials", "false");
String exposeHeaders = "access-control-expose-headers";
// if (!res.containsHeader(exposeHeaders))
res.setHeader(exposeHeaders, "*");
// 处理options请求
if (req.getMethod().toUpperCase().equals("OPTIONS")) {
return;
}
chain.doFilter(request, response);
}
}
启动类中注入过滤器
@SpringBootApplication
@MapperScan("com.woniuxy.ticket.auth.mapper")
public class TicketAuthApplication {
public static void main(String[] args) {
SpringApplication.run(TicketAuthApplication.class, args);
}
@Bean
public FilterRegistrationBean<TicketCrossFilter> registrationFilterBean() {
FilterRegistrationBean<TicketCrossFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new TicketCrossFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setOrder(0);
filterRegistrationBean.setEnabled(true);
return filterRegistrationBean;
}
}
2.9.2 SpringBoot和JSON格式解析的错误
可能会出现以下报错:
no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
这是因为SpringBoot对Json格式的解析,Springboot中有一套对Json格式输出的严格控制—JackSon
问题解决的办法其实就是差在配置文件中:<将jackSon关闭即可>
spring:
jackson:
serialization:
FAIL_ON_EMPTY_BEANS: false