1 加入pom.xml
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2 配置application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot-demo07-security?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.druid.initialSize=5
spring.datasource.druid.minIdle=5
spring.datasource.druid.maxActive=20
spring.datasource.druid.maxWait=60000
spring.datasource.druid.timeBetweenEvictionRunsMillis=60000
spring.datasource.druid.minEvictableIdleTimeMillis=300000
spring.datasource.druid.validationQuery=SELECT 1
spring.datasource.druid.testWhileIdle=true
spring.datasource.druid.testOnBorrow=true
spring.datasource.druid.testOnReturn=false
spring.datasource.druid.poolPreparedStatements=true
spring.datasource.druid.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.druid.filters=stat,wall
spring.datasource.druid.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
spring.datasource.druid.stat-view-servlet.allow=127.0.0.1
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=500
spring.redis.jedis.pool.min-idle=0
spring.redis.lettuce.shutdown-timeout=0
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.auto-mapping-behavior=full
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath*:mapper/*Mapper.xml
server.port=9001
jwt.header=Authorization
jwt.expire=604800
jwt.secret=ji8n3439n439n43ld9ne9343fdfer49h
3 自定义用户认证的核心逻辑
1 创建UserDetailServiceImpl实现UserDetailsService
package com.grm.security;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.grm.entity.SysUser;
import com.grm.enums.CodeEnum;
import com.grm.exception.BusinessException;
import com.grm.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 自定义用户认证的核心逻辑
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
SysUserService sysUserService;
/**
* 需要构造出 org.springframework.security.core.userdetails.User 对象并返回
*
* @param username 用户名
* @return {@link UserDetails}
* @throws UsernameNotFoundException 用户名没有发现异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
SysUser sysUser = sysUserService.getOne(wrapper);
if (sysUser == null) {
throw new BusinessException(CodeEnum.USER_ACCOUNT_NOT_EXIST);
}
return new LoginUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), getUserAuthority(sysUser.getId()));
}
/**
* 获取用户权限信息(角色、菜单权限)
*
* @param userId 用户id
* @return {@link List}<{@link GrantedAuthority}>
*/
public List<GrantedAuthority> getUserAuthority(Long userId) {
// ROLE_admin,ROLE_normal,sys:user:list,....
String authority = sysUserService.getUserAuthorityInfo(userId);
//为用户分配权限,上面的配置类会根据权限来限制访问,产生不同结果。
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
}
这里边,loadUserByUsername方法返回的是security包中的UserDetails对象
所以,我们要自定义LoginUser实现UserDetails返回给loadUserByUsername
package com.grm.security;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* 最终交给Spring Security的是UserDetails
* 用户的权限集, 默认需要添加ROLE_前缀
* 用户的加密后的密码,不加密会使用{noop}前缀
* 应用内唯一的用户名
* 账户是否过期
* 账户是否锁定
* 凭证是否过期
* 用户是否可用
* <p>
* 登录用户实现UserDetails接口
*/
public class LoginUser implements UserDetails {
private Long userId;
private String password;
private final String username;
// 已授予的权限
private final Collection<? extends GrantedAuthority> authorities;
// 账户是否过期
private final boolean accountNonExpired;
// 账户是否锁定
private final boolean accountNonLocked;
// 凭证是否过期
private final boolean credentialsNonExpired;
// 用户是否可用
private final boolean enabled;
public LoginUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId, username, password, true, true, true, true, authorities);
}
public LoginUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
this.userId = userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String getUsername() {
return username;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
但是,用户的权限也得查询出来返回回去,具体体现在UserDetails中的
所以我们要封装一个方法,根据userId查询authorities
其中sysUserService.getUserAuthorityInfo(userId)具体实现如下
package com.grm.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.grm.entity.SysMenu;
import com.grm.entity.SysRole;
import com.grm.entity.SysUser;
import com.grm.mapper.SysMenuMapper;
import com.grm.mapper.SysRoleMapper;
import com.grm.mapper.SysUserMapper;
import com.grm.service.SysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户实现层
*
* @author gaorimao
* @date 2022-02-04
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
private static final Logger LOGGER = LoggerFactory.getLogger(SysUserServiceImpl.class);
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
@Autowired
private SysMenuMapper sysMenuMapper;
@Override
public String getUserAuthorityInfo(Long userId) {
String authority;
// 1.获取角色代码
QueryWrapper<SysRole> sysRoleQueryWrapper = new QueryWrapper<>();
String queryRoleIdsSql = "select role_id from sys_user_role where user_id = " + userId;
sysRoleQueryWrapper.inSql("id", queryRoleIdsSql);
List<SysRole> roles = sysRoleMapper.selectList(sysRoleQueryWrapper);
String roleCodes = "";
if (roles.size() > 0) {
// 查询角色代码
roleCodes = roles.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));
LOGGER.info("roleCodes==================={}",roleCodes);
}
authority = roleCodes.concat(",");
LOGGER.info("authorityRole==================={}",authority);
// 2.获取权限代码
// 2.1 如果角色有admin,默认所有菜单权限
String menuPerms = "";
if (roles.size() > 0) {
List<SysRole> adminRoles = roles.stream().filter(role -> role.getCode().equals("admin")).collect(Collectors.toList());
if (adminRoles.size() > 0) {
List<SysMenu> menus = sysMenuMapper.selectList(null);
// 查询菜单权限
menuPerms = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
}else{
menuPerms = getPermsAuthorityByUserId(userId);
}
LOGGER.info("authorityMenuPerms==================={}",menuPerms);
}
authority = authority.concat(menuPerms);
// ROLE_admin,ROLE_normal,sys:user:list,sys:role:list...
LOGGER.info("authorityAll==================={}",authority);
return authority;
}
/**
* 通过userId查询所有相关的menuIds
*
* @param userId 用户id
* @return {@link String}
*/
private String getPermsAuthorityByUserId(Long userId) {
List<Long> menuIds = sysUserMapper.queryMenuIdsByUserId(userId);
String menuPerms = "";
if (menuIds.size() > 0) {
List<SysMenu> menus = sysMenuMapper.selectBatchIds(menuIds);
// 查询菜单权限
menuPerms = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
}
return menuPerms;
}
}
其中sysUserMapper.queryMenuIdsByUserId(userId)方法需要自己写sql实现下
代码如下
package com.grm.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.grm.entity.SysUser;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface SysUserMapper extends BaseMapper<SysUser> {
@Select("SELECT DISTINCT srm.menu_id " +
"FROM sys_user_role sur LEFT JOIN sys_role_menu srm " +
"ON sur.role_id = srm.role_id " +
"WHERE sur.user_id = #{userId} ")
List<Long> queryMenuIdsByUserId(Long userId);
}
4 配置SecurityConfig
1 开启三个注解,实现两个方法,配置加密方式
package com.grm.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* security配置认证和授权
*
* @author gaorimao
* @since 2022-02-03
*/
@Configuration
//开启Spring Security的功能
@EnableWebSecurity
//prePostEnabled属性决定Spring Security在接口前注解是否可用@PreAuthorize,@PostAuthorize等注解,设置为true,会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] URL_WHITELIST = {
"/login",
"/logout",
};
/**
* 配置认证方式等
*
* @param auth 身份验证
* @throws Exception 异常
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置认证方式等
super.configure(auth);
}
/**
* security配置,包括登入登出、异常处理、会话管理等
*
* @param http http
* @throws Exception 异常
*/
protected void configure(HttpSecurity http) throws Exception {
}
/**
* 加密方式
*
* @return {@link BCryptPasswordEncoder}
*/
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
2 配置认证信息
@Bean
public UserDetailsService userDetailsService() {
//获取用户账号密码及权限信息
return new UserDetailServiceImpl();
}
/**
* 配置认证方式等
*
* @param auth 身份验证
* @throws Exception 异常
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置认证方式
auth.userDetailsService(userDetailsService());
}
3 屏蔽Spring Security默认重定向登录页面以实现前后端分离功能
实现AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。
AuthenticationEntryPoint主要是用来处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效)
package com.grm.security;
import com.alibaba.fastjson.JSON;
import com.grm.common.Result;
import com.grm.enums.CodeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
/**
* 未登录,或者登录状态过期失效
*
* @author gaorimao
* @date 2022-02-04
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Result result = Result.failed(CodeEnum.USER_NOT_LOGIN);
LOGGER.info("[JwtAuthenticationEntryPoint] result = {}", JSON.toJSONString(result));
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
再在SecurityConfig加入认证失败处理的配置
(加入了csrf禁用、关闭session的代码)
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
/**
* security配置,包括登入登出、异常处理、会话管理等
*
* @param http http
* @throws Exception 异常
*/
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF禁用,因为不使用session
.csrf().disable()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
}
4 登录成功,登录失败,退出登录逻辑
(1) 登录成功
package com.grm.security.handler;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.grm.common.Result;
import com.grm.utils.JwtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录成功处理程序
*
* @author gaorimao
* @date 2022-02-04
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginSuccessHandler.class);
@Autowired
private JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
//进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
// 生成jwt,并放置到请求头中
String jwt = jwtUtils.generateToken(authentication.getName());
response.setHeader(jwtUtils.getHeader(), jwt);
Result result = Result.success();
LOGGER.info("[LoginSuccessHandler] result = {}", JSON.toJSONString(result));
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
其中JwtUtils代码如下
package com.grm.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtUtils {
private long expire;
private String secret;
private String header;
// 生成jwt
public String generateToken(String username) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + 1000 * expire);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(expireDate)// 7天過期
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 解析jwt
public Claims getClaimByToken(String jwt) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
return null;
}
}
// jwt是否过期
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
}
(2)登录失败逻辑
package com.grm.security.handler;
import com.alibaba.fastjson.JSON;
import com.grm.common.Result;
import com.grm.enums.CodeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录失败处理程序
*
* @author gaorimao
* @date 2022-02-04
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginFailureHandler.class);
private static final String BAD_CREDENTIALS = "Bad credentials";
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
Result result;
if (BAD_CREDENTIALS.equals(exception.getMessage())) {
result = Result.failed(CodeEnum.COMMON_FAIL);
} else {
result = Result.failed(exception.getMessage());
}
LOGGER.info("[LoginFailureHandler] result = {}", JSON.toJSONString(result));
outputStream.write(JSON.toJSONString(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
(3)退出登录
package com.grm.security.handler;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.grm.common.Result;
import com.grm.utils.JwtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 退出登录成功
*
* @author gaorimao
* @date 2022-02-04
*/
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtLogoutSuccessHandler.class);
@Autowired
private JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
response.setHeader(jwtUtils.getHeader(), "");
Result result = Result.success();
LOGGER.info("[JwtLogoutSuccessHandler] result = {}", JSON.toJSONString(result));
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
把登录成功、登录失败、退出登录加入到SecurityConfig配置中
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private JwtLogoutSuccessHandler logoutSuccessHandler;
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF禁用,因为不使用session
.csrf().disable()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint);
// 登录
http.formLogin().
permitAll().//允许所有用户
successHandler(loginSuccessHandler).//登录成功处理逻辑
failureHandler(loginFailureHandler);//登录失败处理逻辑
//登出
http.logout().
permitAll().//允许所有用户
logoutSuccessHandler(logoutSuccessHandler).//登出成功处理逻辑
deleteCookies("JSESSIONID");//登出之后删除cookie
}