项目搭建—SpringSecurity
每一步直接都是有联系的,若中途出现爆红,等所有配置结束后,就不会爆红了。
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- 第一步 先创建 LoginUser 和 User 两个实体类,一个是用来验证登录的,一个是用来保存用户信息的。并创建 UserMapper
@Data
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true) //要加上这个注解,否则这个User的值从redis绑定过来的时候绑定不上了
public class LoginUser implements UserDetails {
private User user;
private List<String> permissions;
// @JSONField(serialize = false) //不让 List<SimpleGrantedAuthority> authorities 存入到redis,因为SimpleGrantedAuthority不适合序列化
@JsonIgnore
private List<GrantedAuthority> authorities;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities != null){
return authorities;
}
//把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象
authorities = new ArrayList<>();
for(String permission : permissions){
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
return null;
}
@Override
public String getPassword() {
return new BCryptPasswordEncoder().encode(user.getPassword());
// return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable{
//用户id
@JsonSerialize(using = ToStringSerializer.class)
@TableId
private Long id;
//用户名 -- 邮箱
private String username;
//用户密码
private String password;
//身份证
private String identityCard;
//用户性别
private String gender;
//地理位置信息
private String location;
//用户头像
private String userImage;
//用户电话
private String phoneNumber;
//用户创建时间
private String createTime;
//用户修改时间
private String updateTime;
//逻辑删除
@TableLogic
private int isDelete;
}
@Repository
public interface UserMapper extends BaseMapper<User> {
}
- 第二步 创建 Menu 菜单类用于管理角色权限,并创建 MenuMapper 用于查询用户权限,并创建 MenuMapper.xml
@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
private static final long serialVersionUID = -54979041104113736L;
@TableId
private Long id;
/**
* 菜单名
*/
private String menuName;
/**
* 路由地址
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 菜单状态(0显示 1隐藏)
*/
private String visible;
/**
* 菜单状态(0正常 1停用)
*/
private String status;
/**
* 权限标识
*/
private String perms;
/**
* 菜单图标
*/
private String icon;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
/**
* 是否删除(0未删除 1已删除)
*/
private Integer delFlag;
/**
* 备注
*/
private String remark;
}
@Repository
public interface MenuMapper extends BaseMapper<Menu> {
public List<String> selectPermsByUserId(@Param("userId") Long userId);
}
<?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.neu.mapper.MenuMapper">
<select id="selectPermsByUserId" resultType="java.lang.String">
select
DISTINCT m.`perms`
FROM
sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = #{userId}
AND r.`status` = 0
AND m.`status` = 0
</select>
</mapper>
- 第三步 实现一个 UserDetailsServiceImpl 实现类
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Autowired
MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
User user = userMapper.selectOne(wrapper);
//判断了
if(user == null){
throw new UsernameNotFoundException("用户名不存在");
}
List<String> permissions = menuMapper.selectPermsByUserId(user.getId());
//密码比对的那部分,SpringSecurity自动帮你做了
return new LoginUser(user,permissions);//如何去校验用户名和密码的?是通过调用LoginUser中的getUsername和getPassword方法进行校验的
}
}
- 第四步 引入jwt工具类
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "qx";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");//token用UUID来代替
return token;
}
/**
id : 可以不用
subject : 我们想要加密存储的数据
ttl : 我们想要设置的过期时间
*/
/**
* 生成token jwt加密
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成token jwt加密
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 创建token jwt加密
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
public static void main(String[] args) throws Exception {
//jwt加密
String jwt = createJWT("123456");
System.out.println(jwt);
//jwt解密
Claims claims = parseJWT(jwt);
String subject = claims.getSubject();
System.out.println(subject);
System.out.println(jwt);
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* jwt解密
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
- 第五步 创建 LoginAndLogoutService 接口和 LoginAndLogoutServiceImpl 实现类,以及对应的Controller
@Service
public class LoginAndLogoutServiceImpl implements LoginAndLogoutService {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
private RedisTemplate redisTemplate;
@Override
public Result login(String username, String password) {
//登录认证,把用户登录的用户名和密码封装到authenticationToken对象中去
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
//authenticationToken对象中
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//要是验证通过了,这个authenticate对象不会为null的
//验证用户是否登录成功
if(Objects.isNull(authenticate)){
throw new RuntimeException("用户名或密码错误");
}
//到这里就是登录成功了
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String token = JwtUtil.createJWT(userId);
//将用户信息存入Redis
redisTemplate.opsForValue().set(userId,loginUser); //注意这里的LoginUser为什么可以直接存入Redis?是因为UserDetails已经序列化过了
return new Result(200,"登录成功","Token : "+token);
}
@Override
public Result logout() {
//获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
//这里不需要考虑LoginUser存不存在的问题,能进入到这里,都是已经经过了认证的,所以肯定是存在的
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String userId = loginUser.getUser().getId().toString();
//删除redis中的值
redisTemplate.delete(userId);
return new Result(200, "成功退出登录");
}
}
- 第六步 引入 WebUtil 工具类 和 权限异常的全局处理 和 登录异常的全局处理 以及 全局异常处理 的配置
public class WebUtils
{
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
Result result = new Result(HttpStatus.FORBIDDEN.value(), "权限不足");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
Result result = new Result(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}
@ControllerAdvice(annotations = {RestController.class, Controller.class})
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = RuntimeException.class)
public Result handleArgsException(RuntimeException e){
Result result = new Result();
result.setMsg(e.getMessage());
// result.setData(e.getStackTrace());
result.setCode(500);
return result;
}
}
- 第七步 配置一个过滤器,让用户不需要重复进行登录 JwtAuthenticationTokenFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){
//放行
filterChain.doFilter(request,response);
return;//注意这里一定要return,如果这里不加return,当后面的过滤链执行完了之后,回到这个方法后,还会往下面继续执行
}
//解析token
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = userId;
LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//过滤链放行
filterChain.doFilter(request,response);
}
}
-
第八步 创建 SecurityConfig 的配置文件
@EnableGlobalMethodSecurity(prePostEnabled = true) 这一个配置非常重要,配置了才能使用@PreAuthorize注解
import com.zhku.fruitsandvegetables.filter.JwtAuthenticationTokenFilter; 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.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { //自定义权限和认证异常处理 认证失败其实就是登录没通过 http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //配置Token过滤器的位置,放到 UsernamePasswordAuthenticationFilter 之前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置不要使用Session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //开启SpringSecurity的允许跨域的功能 http.cors(); //关闭csrf的防护 http.csrf().disable(); http //开启授权配置 .authorizeRequests() .antMatchers("/login","/logout","/register").permitAll() //这些路径不需要登录就可以访问 .anyRequest().authenticated(); //其他路径都需要身份验证 } }