流程:
(1) 用户通过 URL 请求登录,如 /user/login,一般地,该 URL 需要在 ShiroConfiguration 设定为匿名权限,见目录 5 ,因此放行不会被拦截,如果认证成功,服务端会返回 JWT 给用户。
(2) 浏览器收到 JWT,以后每次请求都需要携带 JWT,并且这个携带 JWT 的行为由开发者自己控制,一般放在请求头中。
(3) 以后每次请求需要授权的 URL 都会经过 JWTFilter,获得 token,然后进入 Realm。
(4) Realm 一般需要进行认证与授权,认证如果有问题,可以自己根据逻辑抛出异常。
目录
1. 数据表
2. UserService
3. UserRealm
4. JWTToken
5. JWTFilter
6. ShiroConfiguration
直接复制 SQL 语句执行即可。
CREATE TABLE `sys_role` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NULL,
`name_zh` varchar(255) NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `sys_role_route` (
`id` int NOT NULL AUTO_INCREMENT,
`role_id` int NULL,
`route_id` int NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `sys_route` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NULL,
`title` varchar(255) NULL,
`path` varchar(255) NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `sys_user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) NULL,
`password` varchar(255) NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `sys_user_role` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NULL,
`role_id` int NULL,
PRIMARY KEY (`id`)
);
UserDO 对应数据表 sys_user
@Data
@TableName("sys_user")
public class UserDO {
private Integer id;
private String username;
private String password;
}
RoleDO 对应数据表 sys_role
@Data
@TableName("sys_role")
public class RoleDO {
private Integer id;
private String name;
private String name_zh;
}
UserRoleDO 对应数据表 sys_user_role
@Getter
@Setter
@TableName("sys_user_role")
public class UserRoleDO {
private Integer id;
private Integer user_id;
private Integer role_id;
}
RouteDO 对应数据表 sys_route
@Data
@TableName("sys_route")
public class RouteDO {
private Integer id;
private String name;
private String path;
private String title;
}
RoleRouteDO 对应数据表 sys_role_route
@Data
@TableName("sys_role_route")
public class RoleRouteDO {
private Integer id;
private Integer role_id;
private Integer route_id;
}
JWT 依赖
com.auth0
java-jwt
3.8.3
Shiro 依赖
org.apache.shiro
shiro-spring
1.4.2
@Service
public class UserService {
@Autowired
UserMapper userMapper;
@Autowired
UserRoleMapper userRoleMapper;
@Autowired
RoleMenuMapper roleMenuMapper;
@Autowired
RoleMapper roleMapper;
@Autowired
String secret;
@Autowired
MenuMapper menuMapper;
public LoginDTO login(LoginBO loginBO) {
UserDO user = userMapper
.selectOne(new QueryWrapper().lambda().eq(UserDO::getUsername, loginBO.getUsername()));
/* (1) 判断用户名是否存在 */
boolean exist = user != null;
if (!exist) {
throw new UnknownAccountException("用户名不存在");
}
/* (2) 判断密码是否错误 */
boolean valid = user.getPassword().equals(loginBO.getPassword());
if (!valid) {
throw new IncorrectCredentialsException("密码错误");
}
/* (3) 获取该user的所有角色 */
List userRoleList = userRoleMapper
.selectList(new QueryWrapper().lambda().eq(UserRoleDO::getUser_id, user.getId()));
List roles = new ArrayList();
userRoleList.forEach((userRole) -> {
RoleDO role = roleMapper.selectById(userRole.getRole_id());
roles.add(role.getName());
});
LoginDTO loginDTO = new LoginDTO();
loginDTO.setToken(JWT.create()
.withClaim("username", user.getUsername())
.withArrayClaim("roles", roles.toArray(new String[roles.size()]))
.withExpiresAt(DateUtil.offsetSecond(new Date(), 60))
.sign(Algorithm.HMAC256(secret)));
return loginDTO;
}
public GetInfoDTO getInfo(GetInfoBO getInfoBO) {
String token = getInfoBO.getToken();
DecodedJWT decodedJWT = JWT.decode(token);
List roles = decodedJWT.getClaim("roles").asList(String.class);
String name = decodedJWT.getClaim("name").asString();
return new GetInfoDTO(roles, name, null, null);
}
public GetMenuDTO getRoutes() {
String token = SecurityUtils.getSubject().getPrincipal().toString();
List roles = JWT.decode(token).getClaim("roles").asList(String.class);
GetMenuDTO dto = new GetMenuDTO();
List menus = new ArrayList();
roles.forEach((role) -> {
RoleDO roleDO = roleMapper.selectOne(new QueryWrapper().lambda().eq(RoleDO::getName, role));
List roleMenus = roleMenuMapper
.selectList(new QueryWrapper().lambda().eq(RoleRouteDO::getRole_id, roleDO.getId()));
roleMenus.forEach((roleMenu) -> {
RouteDO menuDO = menuMapper
.selectOne(new QueryWrapper().lambda().eq(RouteDO::getId, roleMenu.getRoute_id()));
menus.add(menuDO);
});
});
dto.setMenus(menus);
return dto;
}
}
@Component
public class UserRealm extends AuthorizingRealm {
@Autowired
UserMapper userMapper;
@Autowired
String secret;
@Autowired
UserRoleMapper userRoleMapper;
@Autowired
RoleMapper roleMapper;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String token = SecurityUtils.getSubject().getPrincipal().toString();
String name = JWT.decode(token).getClaim("name").toString();
UserDO user = userMapper.selectOne(new QueryWrapper().lambda()
.eq(UserDO::getUsername, name));
UserRoleDO userRole = userRoleMapper.selectList(new QueryWrapper().lambda()
.eq(UserRoleDO::getUser_id, user.getId())).get(0);
RoleDO role = roleMapper.selectById(userRole.getRole_id());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole(role.getName());
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String name = token.getPrincipal().toString();
String password = token.getCredentials().toString();
// 解密获得username,用于和数据库进行对比
if (name == null) {
throw new AuthenticationException("token无效");
}
UserDO user = userMapper.selectOne(new QueryWrapper().lambda().eq(UserDO::getUsername, name));
if (user == null) {
throw new AuthenticationException("用户不存在!");
}
user = userMapper.selectOne(
new QueryWrapper().lambda().eq(UserDO::getUsername, name).eq(UserDO::getPassword, password));
if (user == null) {
throw new AuthenticationException("用户名或密码错误!");
}
return new SimpleAuthenticationInfo(name, password, getName());
}
}
public class JWTToken implements AuthenticationToken {
private static final long serialVersionUID = -1324191236962615571L;
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return JWT.decode(token).getClaim("name").asString();
}
@Override
public Object getCredentials() {
return JWT.decode(token).getClaim("password").asString();
}
}
public class JWTFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
return executeLogin(request, response);
} catch (Exception e) {
return false;
}
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
JWTToken token = new JWTToken(httpServletRequest.getHeader("X-Token"));
try {
/* 接下来会进入 Realm 接口进行检验 */
getSubject(request, response).login(token);
} catch (AuthenticationException e) {
ResponseDTO responseDTO = ResponseDTO.failure("无访问权限: " + e.getMessage());
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSONUtil.toJsonStr(responseDTO));
return false;
}
return true;
}
@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);
}
}
@Configuration
public class ShiroConfiguration {
@Autowired
UserRealm userRealm;
@Bean
public String secret() {
return UUID.randomUUID().toString();
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map filterChainDefinitionMap = new LinkedHashMap();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/user/logout", "anon");
filterChainDefinitionMap.put("/user/info", "anon");
filterChainDefinitionMap.put("/mission", "anon");
filterChainDefinitionMap.put("/master/status", "anon");
filterChainDefinitionMap.put("/import", "anon");
// 添加自己的过滤器并且取名为jwt
Map filterMap = new HashMap(1);
filterMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//