简介
- SpringSecurity是一个用于Java 企业级应用程序的安全框架(简单说是对访问权限进行控制)
- 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)
认证过程
Spring Security 的认证过程
- (前端)用户使用用户名和密码进行登录。
- (后端)Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken。
- (后端)将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证。
- (后端)AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。
- (后端)通过调用 SecurityContextHolder.getContext().setAuthentication(…) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext。
在认证成功后,用户就可以继续操作去访问其它受保护的资源了,但是在访问的时候将会使用保存在 SecurityContext 中的 Authentication 对象进行相关的权限鉴定。
流程展示
数据库: mysql 8.x
连接池: druid
持久层框架: MyBatis-plus
安全框架: Spring Security
安全传输工具: JWT
Json解析: fastjson
pom.xml
中,引入相关依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.3.3version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.11.4version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.11version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>swagger-bootstrap-uiartifactId>
<version>1.9.6version>
dependency>
<dependency>
<groupId>com.github.pengglegroupId>
<artifactId>kaptchaartifactId>
<version>2.3.2version>
dependency>
application.yml
# 开发环境配置文件
server:
port: 9090
spring:
# 配置数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #数据库链接驱动
url: jdbc:mysql://localhost:3306/base?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai #url
username: root #用户名
password: root1234 #密码
#Druid数据源配置
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 10 #初始化时建立物理连接的个数
min-idle: 5 #最小连接池数量
maxActive: 30 #最大连接池数量
maxWait: 60000 #获取连接时最大等待时间,单位毫秒
# mybatis-plus 配置
mybatis-plus:
# 配置mapper映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
type-aliases-package: com.fang.system.entity
configuration:
# 自动驼峰命名
map-underscore-to-camel-case: true
# mybatis sql打印(方法接口所在的包,不是mapper.xml文件所在的包)
logging:
level:
com.example.admin.mapper: debug
# Redis配置
redis:
host: 127.0.0.1 #服务器地址
port: 6379 #服务器端口
database: 0 #数据库
password: #密码
timeout: 100000ms #超时时间
lettuce:
pool:
max-active: 1024 #最大连接数
max-wait: 10000ms #最大连接阻塞时间 默认-1
max-idle: 200 #最大空闲连接
min-idle: 5 #最小空闲连接
# jwt 配置
jwt:
tokenHeader: Authorization
tokenHead: Bearer # Token前缀字符
expiration: 604800 #7天,秒单位
secret: base-security-secret # 密匙KEY
登录验证:
用户登陆,会被
AuthenticationProcessingFilter
拦截,调用AuthenticationManager
的实现,AuthenticationManager
会调用ProviderManager
来获取用户验证信息(不同的Provider调用的服务不同,因为这些信息可以是在数据库上,可以是在LDAP服务器上,可以是xml配置文件上等);如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存
SecurityContextHolder
中,以备后面访问资源时使用。
访问资源(即授权管理):访问资源(即授权管理):
访问资源(即授权管理),访问url时,会通过
AbstractSecurityInterceptor
拦截器拦截,其中会调用FilterInvocationSecurityMetadataSource
的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager
,这个授权管理器会通过spring的全局缓存SecurityContextHolde
r获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,配置权限策略,如果权限足够,则返回,权限不够则报错并调用权限不足页面。
数据库表设计
权限过滤器 CustomAccessFilter.java
package com.example.admin.filter;
import com.example.admin.entity.Menu;
import com.example.admin.entity.Role;
import com.example.admin.service.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
/**
* 权限控制
* @author fangqi174956
*/
@Component
public class CustomAccessFilter implements FilterInvocationSecurityMetadataSource {
@Autowired
private IMenuService menuService;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 获取请求的url
String requestUrl = ((FilterInvocation) object).getRequestUrl();
List<Menu> menus = menuService.getMenusWithRole();
for (Menu menu : menus) {
// 判断请求url 与菜单角色是否匹配
if(antPathMatcher.match(menu.getUrl(),requestUrl)){
String[] roleList = menu.getRoles().stream().map(Role::getUniqueCode).toArray(String[]::new);
return SecurityConfig.createList(roleList);
}
}
// 没有匹配上的url 默认角色ROLE_login (即登录即可访问)
return SecurityConfig.createList("ROLE_login");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
权限控制:判断用户角色 CustomUrlDecisionManager.java
package com.example.admin.filter;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* 权限控制
* 判断用户角色
* @author fangqi174956
*/
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : configAttributes) {
// 当前url所需要的角色
String needRole = configAttribute.getAttribute();
// 判断角色是否是 登录即可访问的角色,此角色是在CustomAccessFilter中设置的
if ("ROLE_login".equals(needRole)) {
if(authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("尚未登录,请先登录");
}else {
return;
}
}
// 判断角色是否为url所需要角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足!请联系管理员");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return false;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
然后需要在 SecurityConfig.java
配置文件中,配置动态权限控制
/**
* 配置策略
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用jwt,不需要csrf
http.csrf().disable()
//基于token,不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 其他所有请求都需要认证
.anyRequest()
.authenticated()
// 动态权限配置
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customAccessFilter);
return object;
}
})
// 禁用缓存
.and()
.headers()
.cacheControl();
//添加jwt 登录授权过滤器
http.addFilterBefore(jwtAuthorizationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
JwtTokenUtil.java
package com.example.admin.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* jwt-token 工具类
* @author fangqi174956
*/
@Component
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 根据用户信息生成token
* @param userDetails
* @return
*/
public String createGenerateToken(UserDetails userDetails){
Map<String,Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED,new Date());
// 调用私有方法myGenerateToken() 根据荷载生成token
return myGenerateToken(claims);
}
/**
* 根据荷载生成token
* @param claims
* @return
*/
private String myGenerateToken(Map<String,Object> claims){
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512,secret)
.compact();
}
/**
* 生成token失效时间
* @return
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 解析token 从token中获取登录用户名
* @param token
* @return
*/
public String getUserNameFromToken(String token){
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 从token中获取荷载
* @param token
* @return
*/
private Claims getClaimsFromToken(String token){
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* 验证token是否过期
* @param token
* @param userDetails
* @return
*/
public boolean validateToken(String token,UserDetails userDetails){
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token 是否失效
* @param token
* @return
*/
private boolean isTokenExpired(String token) {
Date expireDate = getExpireDateFromToken(token);
return expireDate.before(new Date());
}
/**
* 从token中获取失效时间
* @param token
* @return
*/
private Date getExpireDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 刷新token
* @param token
* @return
*/
public String refreshToken(String token){
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED,new Date());
return myGenerateToken(claims);
}
}
CustomAuthorityDeserializer.java
package com.example.admin.config.security;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* 自定义Authority反序列化解析器
* @author fangqi174956
*/
public class CustomAuthorityDeserializer extends JsonDeserializer {
/**
* 反序列号的方法
* @param jsonParser
* @param deserializationContext
* @return
* @throws IOException
* @throws JsonProcessingException
*/
@Override
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
JsonNode jsonNode = mapper.readTree(jsonParser);
List<GrantedAuthority> grantedAuthorities = new LinkedList<>();
// 树型节点的所有元素
Iterator<JsonNode> elements = jsonNode.elements();
while (elements.hasNext()){
JsonNode next = elements.next();
JsonNode authority = next.get("authority");
grantedAuthorities.add(new SimpleGrantedAuthority(authority.asText()));
}
return null;
}
}
JwtAuthorizationTokenFilter.java
package com.example.admin.config.security;
import cn.hutool.core.util.StrUtil;
import com.example.admin.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* jwt登录授权过滤器
* @author fangqi174956
*/
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// tokenHeader: Authorization
String authHeader = request.getHeader(tokenHeader);
//存在token token="Bearer eyBdafdadfdfasdfdfasdfasd....."
if(StrUtil.isNotBlank(authHeader) && authHeader.startsWith(tokenHead)){
String authToken = authHeader.substring(tokenHead.length());
//从token 中获取当前登录用户名
String username = jwtTokenUtil.getUserNameFromToken(authToken);
//token存在用户名但未登录
if(StrUtil.isNotBlank(username) && SecurityContextHolder.getContext().getAuthentication() == null){
//登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//验证token是否有效,重新设置用户对象
if(jwtTokenUtil.validateToken(authToken,userDetails)){
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request,response);
}
}
RestAuthorizationEntryPoint.java
package com.example.admin.config.security;
import com.example.admin.common.RestResult;
import com.example.admin.constant.HttpStatusConstants;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 当未登录 或者 token失效时,访问接口的自定义的返回结果
* @author fangqi174956
*/
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter out = response.getWriter();
RestResult restResult = RestResult.error("尚未登录,请登录!");
restResult.setCode(HttpStatusConstants.CODE_401);
out.write(new ObjectMapper().writeValueAsString(restResult));
out.flush();
out.close();
}
}
RestfulAccessDeniedHandler.java
package com.example.admin.config.security;
import com.example.admin.common.RestResult;
import com.example.admin.constant.HttpStatusConstants;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 当访问接口没有权限时,自定义返回结果
* @author fangqi174956
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter out = response.getWriter();
RestResult restResult = RestResult.error("权限不足,请联系管理员!");
restResult.setCode(HttpStatusConstants.CODE_403);
out.write(new ObjectMapper().writeValueAsString(restResult));
out.flush();
out.close();
}
}
SecurityConfig.java
package com.example.admin.config.security;
import cn.hutool.core.util.ObjectUtil;
import com.example.admin.entity.Admin;
import com.example.admin.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* SpringSecurity 配置类
* @author fangqi174956
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* @Autowired 构造器注入
*/
private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Autowired
public SecurityConfig(RestAuthorizationEntryPoint restAuthorizationEntryPoint,
RestfulAccessDeniedHandler restfulAccessDeniedHandler) {
this.restAuthorizationEntryPoint = restAuthorizationEntryPoint;
this.restfulAccessDeniedHandler = restfulAccessDeniedHandler;
}
/**
* 解决 无法直接注入 AuthenticationManager
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 放行一些路径 静态资源 不走拦截链
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
//设置直接放行当接口或者资源
web.ignoring().antMatchers(
"/captcha",
"/redis/captcha",
"/login",
"/css/**",
"/js/**",
"/index.html",
"favicon.ico",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/ws/**"
);
}
/**
* 配置策略
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用jwt,不需要csrf
http.csrf().disable()
//基于token,不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 其他所有请求都需要认证
.anyRequest()
.authenticated()
// 禁用缓存
.and()
.headers()
.cacheControl();
//添加jwt 登录授权过滤器
http.addFilterBefore(jwtAuthorizationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
/**
* 使用重写的loadUserByUsername()进行登录验证
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置UserDetailsService 使用BCrypt进行密码的hash
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter(){
return new JwtAuthorizationTokenFilter();
}
/**
* 登陆身份认证
* @return
*/
@Bean
@Override
public UserDetailsService userDetailsService(){
return new UserDetailsService() {
@Autowired
private IAdminService adminService;
// 重写loadUserByUsername方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("loadUserByUsername --> " + username);
// 根据登录用户名查询用户信息
Admin admin = adminService.getAdminByUsername(username);
if(ObjectUtil.isNull(admin)){
// 当前用户不存在
throw new UsernameNotFoundException("当前登录用户:" + username + "不存在!");
}
if(!admin.getStatus()){
// 当前用户已被禁用 status=false
throw new UsernameNotFoundException("该账号被禁用,请联系管理员!");
}
// 查询当前登录用户所拥有的权限(角色)
System.out.println("=== 查询当前登录用户所拥有的权限(角色)===" + adminService.getRolesByAdminId(admin.getId()));
admin.setRoles(adminService.getRolesByAdminId(admin.getId()));
return admin;
}
};
}
}
GlobalExceptionHandler.java
package com.example.admin.excpeption;
import com.example.admin.common.RestResult;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理
* @author fangqi174956
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* sql 操作数据库异常
*/
@ExceptionHandler(SQLException.class)
public RestResult mySqlExceptionHandler(SQLException e){
if (e instanceof SQLIntegrityConstraintViolationException) {
return RestResult.error("该数据有关联数据,操作失败!");
}
return RestResult.error("数据库异常,操作失败!");
}
@ExceptionHandler(RuntimeException.class)
public RestResult loginExceptionHandler(RuntimeException e){
if(e instanceof UsernameNotFoundException){
return RestResult.error(e.getMessage());
}
return RestResult.error("未知错误,请重新登录!");
}
}
LoginController.java
package com.example.admin.controller.login;
import com.example.admin.common.LoginEntity;
import com.example.admin.common.RestResult;
import com.example.admin.service.IAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录控制器
* @author fangqi174956
*/
@Api(tags = "【登录】Controller")
@RestController
public class LoginController {
@Autowired
private IAdminService adminService;
@ApiOperation(value = "登录成功返回token")
@PostMapping("/login")
public RestResult login(@RequestBody LoginEntity loginEntity){
return adminService.login(loginEntity);
}
}
IAdminService.java
public interface IAdminService extends IService<Admin> {
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
Admin getAdminByUsername(String username);
/**
* 根据adminId 查询当前登录用户所拥有的角色
* @param adminId
* @return List
*/
List<Role> getRolesByAdminId(Integer adminId);
/**
* 登录成功返回token
* @param loginEntity
* @return
*/
RestResult login(LoginEntity loginEntity);
/**
* 获取当前登录用户信息
* @param principal
* @return
*/
RestResult getAdminInfo(Principal principal);
}
AdminServiceImpl.java
package com.example.admin.service.impl;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.admin.common.LoginEntity;
import com.example.admin.common.RestResult;
import com.example.admin.constant.HttpStatusConstants;
import com.example.admin.entity.Admin;
import com.example.admin.entity.Role;
import com.example.admin.mapper.AdminMapper;
import com.example.admin.mapper.RoleMapper;
import com.example.admin.service.IAdminService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.admin.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.security.Principal;
import java.util.List;
/**
*
* 系统用户表 服务实现类
*
*
* @author Auth:xxx
* @since 2022-06-17
*/
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
@Autowired
private AdminMapper adminMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
@Override
public Admin getAdminByUsername(String username) {
return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username",username));
}
/**
* 查询当前登录用户所拥有的角色
* @param adminId
* @return List
*/
@Override
public List<Role> getRolesByAdminId(Integer adminId) {
return roleMapper.getRolesByAdminId(adminId);
}
/**
* 登录成功返回token
* @param loginEntity
* @return
*/
@Override
public RestResult login(LoginEntity loginEntity) {
System.out.println("==== 正在验证登录 ---> " + loginEntity + "tokenHead -->" + tokenHead);
// 参数校验
if(StrUtil.isBlank(loginEntity.getUsername())){
throw new UsernameNotFoundException("参数错误!");
}
System.out.println("密码---> " + passwordEncoder.encode(loginEntity.getPassword()));
// 登录校验
UserDetails userDetails = userDetailsService.loadUserByUsername(loginEntity.getUsername());
// 校验密码
boolean verifyPassword = passwordEncoder.matches(loginEntity.getPassword(),userDetails.getPassword());
if(!verifyPassword){
return RestResult.error("用户名或者密码不正确!");
}
/**
* 更新 security 登录用户对象
* 1,用户对象 : userDetails
* 2,凭证(密码): 通常设置为null
* 3, 用户角色(权限) : userDetails.getAuthorities()
*/
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 登录成功 生成token
String token = jwtTokenUtil.createGenerateToken(userDetails);
return RestResult.success("登录成功!",
MapUtil.builder()
.put("token",token)
.put("tokenHead",tokenHead)
.build()
);
}
/**
* 获取当前登录用户信息
* @return
*/
@Override
public RestResult getAdminInfo(Principal principal) {
if(ObjectUtil.isNull(principal)){
return RestResult.error(HttpStatusConstants.CODE_401,"当前登录已过期,请重写登录!");
}
String username = principal.getName();
Admin admin = getAdminByUsername(username);
// 密码保护
admin.setPassword(null);
// 获取当前登录用户拥有的权限(角色)
admin.setRoles(getRolesByAdminId(admin.getId()));
return RestResult.success(
MapUtil.builder()
.put("adminInfo",admin)
.build()
);
}
}
RoleMapper.java
在mapper层中定义方法public interface RoleMapper extends BaseMapper<Role> {
/**
* 查询当前登录用户所拥有的角色
* @param adminId
* @return List
*/
List<Role> getRolesByAdminId(Integer adminId);
}
在RoleMapper.xml
中,执行sql查询方法
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.admin.mapper.RoleMapper">
<resultMap id="BaseResultMap" type="com.example.admin.entity.Role">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="unique_code" property="uniqueCode" />
<result column="remark" property="remark" />
resultMap>
<sql id="Base_Column_List">
id, name, unique_code, remark
sql>
<select id="getRolesByAdminId" resultType="com.example.admin.entity.Role">
SELECT
r.id,
r.`name`,
r.unique_code,
r.remark
FROM
sys_role r
LEFT JOIN sys_admin_role AS ar ON r.id = ar.role_id
WHERE
ar.admin_id = #{adminId}
select>
mapper>
{
"code": 500,
"msg": "当前登录用户:tom1不存在!",
"data": null
}
(2)当前用户被禁用
{
"code": 500,
"msg": "该账号被禁用,请联系管理员!",
"data": null
}
(3) 用户名或密码错误
{
"code": 500,
"msg": "用户名或者密码不正确!",
"data": null
}
鉴权:
例:用户 test --> 拥有角色 ROLE_test ;
ROLE_test 对应的菜单
[ { name: “用户管理”,url:“/sys/admin/“},{ name: “角色管理”,url:”/sys/role/”},{ name: “菜单管理”,url:“/sys/menu/**”} ]
测试结果:
登录test,测试访问接口 /sys/admin/**
测试访问接口 /data/file/** 该接口url不在该用户角色对应的菜单中,则无权限访问