本文主要记录自己的学习历程。
略
观看此文应当具备一定的前后台分离的知识和了解MVC分层开发的概念,以及了解JWT(JsonWebToken),和无状态登录的概念,因笔者水平原因,有说的不对的地方,希望有大佬能够斧正。
首先为了模拟前后台分离开发过程中前台对后台的请求,我们使用Postman进行模拟,后台工程使用springboot整合SpringSecurity以及JWT(JsonWebToken)等,对于数据库的操作使用MyBatis-Plus进行操作,到此准备工作基本完成。
构建一个SpringBoot项目
可参考依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.3.4version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
<dependency>
<groupId>javax.xml.bindgroupId>
<artifactId>jaxb-apiartifactId>
<version>2.3.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-testartifactId>
<scope>testscope>
dependency>
更改配置文件application.yml
server:
port: 8991
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
druid:
test-on-borrow: true
test-while-idle: true
mybatis-plus:
global-config:
db-config:
id-type: auto
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
这里我设置后台工程的端口号为8991,其余配置为数据库的连接信息,以及设置使用druid的数据源和开启sql的日志以便于我们debug
此文仅仅作为一个demo演示,因此我们只需要创建一个User表即可
表字段:
id 为主键 int 类型设置自增即可
username 用户名 可以设置此字段不可重复
password 用户密码字段 长度设置为300是因为 我们会把用户的密码加密存储到表中 所以字段长度大一些
role 用户的角色,对于用户的鉴权操作我们要基于此字段
以上就是数据库的所有信息。
首先编写User实体类
package com.mrr.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {
// 设置主键自增策略
@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private String role;
}
使用lombok插件可以快速帮我们生成getter,setter方法
@Accessors(chain = true) 此注解是用来开启链式编程的(没什么太大的用处,就是为了方便写代码,不了解也没关系)
使用mybatis-plus可以帮助我们快速构建dao层
创建UserMapper接口继承BaseMapper即可(BaseMapper需要传入User实体类的泛型)
package com.mrr.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mrr.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface UserMapper extends BaseMapper<User> {
}
mybatis-plus已经为我们实现了很多的操作,我们不需要再写抽象方法了
本文为了演示方便,就不在写service层了,直接在controller层调用mapper层。
因为前后台分离过程中,他们之间需要通过JSON字符串传递信息。因此为了便于整理,我们写一个模板。
根据我的个人习惯会创建一个包templates,在这个包下创建一个类BaseBackRes
其中的额代码:
package com.mrr.templates;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class BaseBackRes<T> {
private Integer code;
private String message;
private T data;
}
code 为我们定义的返回状态码
message 为我们返回的信息
data 我们可以封装一些信息放在里面
到此返回模板的构造工作也已经完成。
接下来是我们的主要部分
首先理清一下逻辑,首先前台的登陆页面肯定是一个表单,提交表单后会请求后台的登录接口,请求的参数就是表单封装的对象,使用SpringSecurity完成登录操作,我们要做的是,根据前台传过来的用户名
去数据库进行查找,如果能查找出字段就在判断密码是否正确,以及后续的操作(这些都是SpringSecurity来做)
因此需要我们构建一个UserDetailServiceImpl去实现UserDetaiService接口,这个接口是SpringSecurity封装的,里面只有一个方法
创建一个类UserDetailServiceImpl
package com.mrr.config;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mrr.mapper.UserMapper;
import com.mrr.pojo.DetailUser;
import com.mrr.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
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;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
// 注入我们的mapper层
@Autowired
UserMapper userMapper;
// 重写方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
User user = userMapper.selectOne(wrapper);
return new DetailUser(user);
}
}
注意我们返回的不是User对象,那是因为这个方法需要返回一个实现了UserDetails接口的类
因此我们构建一个DetailUser
package com.mrr.pojo;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
@Data
@Accessors(chain = true)
public class DetailUser implements UserDetails {
// 对应我们User的id username password字段即可此处不需要对应role字段
private Integer id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public DetailUser() {
}
// 写一个能直接使用user创建DetailUser的构造器
public DetailUser(User user) {
id = user.getId();
username = user.getUsername();
password = user.getPassword();
authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
}
// 获取权限信息,我们存role字段(可以忽略)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
// 账号是否未过期,默认是false,记为true
@Override
public boolean isAccountNonExpired() {
return true;
}
// 账号是否未锁定,默认是false,记为true
@Override
public boolean isAccountNonLocked() {
return true;
}
// 账号凭证是否未过期,默认是false,记为true
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//默认是false,记为true
@Override
public boolean isEnabled() {
return true;
}
// toString
@Override
public String toString() {
return "DetailUser{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", authorities=" + authorities +
'}';
}
}
这个类我们用来做登录校验使用
接下来是构建另外一个重点
此处代码,笔者参考了网上的教程,基本思想都差不多,大家复制即可
package com.mrr.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
public class JwtTokenUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String SECRET = "jwtsecretdemo";
private static final String ISS = "echisan";
// 过期时间是1个小时
private static final long EXPIRATION = 3600L;
// 选择了记住我之后的过期时间为7天
private static final long EXPIRATION_REMEMBER = 604800L;
// 添加角色前缀
private static final String ROLE_CLAIMS = "rol";
// 创建token
public static String createToken(String username,String role, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setClaims(map)
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
}
// 从token中获取用户名
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}
// 获取用户角色
public static String getUserRole(String token){
return (String) getTokenBody(token).get(ROLE_CLAIMS);
}
// 是否已过期
public static boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}
过滤器我们需要构建两个,第一个是做认证的,此文中指的是登录认证,另一个是鉴权的
首先构建认证过滤器JWTAuthenticationFilter它需要实现UsernamePasswordAuthenticationFilter接口
package com.mrr.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mrr.pojo.DetailUser;
import com.mrr.pojo.LoginUser;
import com.mrr.templates.BaseBackRes;
import com.mrr.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
// 记住我功能,本文暂未实现,但是先写出来
private ThreadLocal<Integer> rememberMe = new ThreadLocal<>();
// 固定代码
private AuthenticationManager authenticationManager;
// 一个构造方法
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
// 这里指定登录请求的接口(我们可以把这个类当作登录功能的controller层)不指定默认为 /login
super.setFilterProcessesUrl("/users/login");
}
// 核心方法
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 从输入流中获取到登录的信息
try {
LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
rememberMe.set(loginUser.getRememberMe() == null ? 0 : loginUser.getRememberMe());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
// 成功验证后调用的方法
// 如果验证成功,就生成token并返回
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 调用getPrincipal()方法会返回一个实现了UserDetails接口的对象,所以这里我们就返回上文定义的DetailUser
response.setContentType("application/json;charset=utf-8");
DetailUser detailUser = (DetailUser) authResult.getPrincipal();
boolean isRemember = rememberMe.get() == 1;
// 获取用户角色信息
String role = "";
Collection<? extends GrantedAuthority> authorities = detailUser.getAuthorities();
for (GrantedAuthority authority : authorities){
role = authority.getAuthority();
}
// 登陆成功,生成JWT并放在响应头里,并封装成JSON格式返回给前台
String token = JwtTokenUtils.createToken(detailUser.getUsername(), role,isRemember);
response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
// 我们前面定义的模板类,把密码置空,防止隐私信息暴露
BaseBackRes res = new BaseBackRes().setCode(200).setMessage("登录成功").setData(((DetailUser) authResult.getPrincipal()).setPassword(null));
writer.write(new ObjectMapper().writeValueAsString(res));
writer.flush();
writer.close();
}
// 这是验证失败时候调用的方法(登陆失败)
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
// 提示用户名或密码错误就行了
BaseBackRes res = new BaseBackRes().setCode(408).setMessage("用户名或密码错误");
writer.write(new ObjectMapper().writeValueAsString(res));
writer.flush();
writer.close();
}
}
具体的含义请移步至代码的注释部分
LoginUser
package com.mrr.pojo;
public class LoginUser {
private String username;
private String password;
private Integer rememberMe;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getRememberMe() {
return rememberMe;
}
public void setRememberMe(Integer rememberMe) {
this.rememberMe = rememberMe;
}
}
到此为止,第一个认证的过滤器就已经实现好了。
下面我们来实现第二个授权过滤器 JWTAuthorizationFilter
实现这个的方式有很多,但是本文就是用这一种,大家实现的方式基本都是大差不差,读者直接复制就行
package com.mrr.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mrr.exception.TokenIsExpiredException;
import com.mrr.templates.BaseBackRes;
import com.mrr.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
// 如果请求头中没有Authorization信息则直接结束
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
try {
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
} catch (Exception e) {
Throwable cause = e.getCause();
//返回json形式的错误信息
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
if(cause instanceof TokenIsExpiredException){
PrintWriter writer = response.getWriter();
BaseBackRes res = new BaseBackRes().setCode(410).setMessage("服务器错误,稍后再试");
writer.write(new ObjectMapper().writeValueAsString(res));
writer.flush();
writer.close();
return;
}
return;
}
super.doFilterInternal(request, response, chain);
}
// 这里从token中获取用户信息并新建一个token
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws TokenIsExpiredException {
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
boolean expiration = JwtTokenUtils.isExpiration(token);
if (expiration) {
throw new TokenIsExpiredException("token超时");
} else {
String username = JwtTokenUtils.getUsername(token);
String role = JwtTokenUtils.getUserRole(token);
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null,
Collections.singleton(new SimpleGrantedAuthority(role))
);
}
}
return null;
}
}
此处有些难懂,笔者的理解也不是很深刻,但是这样已经可以实现鉴权操作了,有兴趣的读者下去之后可以继续研究
TokenIsExpiredException类
package com.mrr.exception;
public class TokenIsExpiredException extends Exception {
public TokenIsExpiredException() {
}
public TokenIsExpiredException(String message) {
super(message);
}
public TokenIsExpiredException(String message, Throwable cause) {
super(message, cause);
}
public TokenIsExpiredException(Throwable cause) {
super(cause);
}
public TokenIsExpiredException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
好的这个时候我们已经写的差不多了,接下来是最后一个重点,也是核心配置类SecurityConfig
package com.mrr.config;
import com.mrr.filter.JWTAuthenticationFilter;
import com.mrr.filter.JWTAuthorizationFilter;
import com.mrr.handler.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* @EnableWebSecurity 此注解必须加上
* @Configuration 此注解必须加上
* @EnableGlobalMethodSecurity(prePostEnabled = true) 配置这个注解后,可以方便我们管理什么样的角色才能访问接口
*/
// UserDetailsService的实现类太多,我们要注入自己的
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
// 用户未登录处理器
@Autowired
private NoEntryPoint noEntryPoint;
// 用户没有权限的处理器
@Autowired
private NoPermissionHandler noPermissionHandler;
// 用户的密码加密方式,我们使用Spring推荐的这个就可以了
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// 设置密码加密器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
// 配置权限的优先级别 此处指的是admin的权限大于user 即高权限角色应当自动拥有低权限角色的权限(需要加上ROLE_前缀)
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return hierarchy;
}
// 配置的核心方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
// 权限设置管理
.authorizeRequests()
// tests下的所有接口,只有有ROLE_ADMIN角色的用户才能访问
.antMatchers(HttpMethod.DELETE, "/tests/**").hasAuthority("ROLE_ADMIN")
// 此处代表放行的资源路径
.antMatchers("/users/login", "/users/register").permitAll()
// 所有请求都需要授权除了放行的(此处的授权指的是登录)
.anyRequest().authenticated() // 这句话的意思是所有请求都需要登录,除了上面已经放行的
.and()
// 没有登录的处理器
.httpBasic().authenticationEntryPoint(noEntryPoint)
.and()
// 权限不足的处理器
.exceptionHandling().accessDeniedHandler(noPermissionHandler)
.and()
//开启表单登录
.formLogin()
.and()
// 使用我们自己的过滤器覆盖默认的,要不然我们的不生效
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
// 一些允许跨域的操作
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
详细含义,请移步至注释
到此为止,我们关于SpringSecurity的配置就差不多了
新建一个包handler
用户未登录处理器 NoEntryPoint
package com.mrr.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mrr.templates.BaseBackRes;
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;
@Component
public class NoEntryPoint implements AuthenticationEntryPoint {
/**
* 没有登录的处理器
* @param request
* @param response
* @param authException
* @throws IOException
* @throws ServletException
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
BaseBackRes res = new BaseBackRes().setCode(409).setMessage("请先登录");
writer.write(new ObjectMapper().writeValueAsString(res));
writer.flush();
writer.close();
}
}
用户没有权限的处理器 NoPermissionHandler
package com.mrr.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mrr.templates.BaseBackRes;
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;
@Component
public class NoPermissionHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
BaseBackRes res = new BaseBackRes().setCode(409).setMessage("您没有此权限");
writer.write(new ObjectMapper().writeValueAsString(res));
writer.flush();
writer.close();
}
}
package com.mrr.controller;
import com.mrr.mapper.UserMapper;
import com.mrr.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@RestController
@RequestMapping("/users")
public class UserController {
// 注入mapper层 不在写service层了
@Autowired
private UserMapper userMapper;
// 注入密码加密器
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMapping("/register")
public HashMap<String, Object> registerUser(@RequestBody User registerUser){
// 密码入数据库之前先加密
registerUser.setPassword(bCryptPasswordEncoder.encode(registerUser.getPassword()));
// 用户注册默认只有USER角色
registerUser.setRole("ROLE_USER");
// 入库
int insert = userMapper.insert(registerUser);
// 返回结果这里就用map模拟了,不在构建模板类了
HashMap<String, Object> map = new HashMap<>();
if(insert > 0){
map.put("code",200);
map.put("message","注册成功");
return map;
}
map.put("code",406);
map.put("message","注册失败");
return map;
}
}
具体含义,请移步至注释
package com.mrr.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/tests")
public class TestController {
@PreAuthorize("hasRole('ADMIN')") // 此注解可以快速指定什么样的请求路径需要什么样的角色,因为我们在SecurityConfig中开启了这个注解
@GetMapping("/getList")
public String getList(){
return "得到一个列表";
}
@PostMapping("createList")
public String createList(){
return "创建一个列表";
}
@PutMapping("/updateList/{testId}")
public String updateList(@PathVariable("testId")Integer id){
return "更新id为:"+id+"的列表";
}
@DeleteMapping("/deleteList/{testId}")
public String deleteList(@PathVariable("testId")Integer id){
return "删除id为:"+id+"的列表";
}
}
使用PostMan进行测试
登录没有问题,并且可以在响应头中看到我们颁发的这个Token
这些请求需要先复制刚才的Token(注意不要复制前面的Bearer)
请求我们的测试接口时放在请求头中
admin2账户没有ROLE_ADMIN角色因此,提示没有权限
createList接口不需要ROLE_ADMIN权限,因此请求成功。
手动修改admin2用户的角色为ROLE_ADMIN,在进行测试
请求成功(这个测试需要再次重新登录)
剩下的接口不在进行测试,原理都是一样的。笔者在此就省略了。
安全校验的内容主要分为两块,认证和授权。只要掌握这个方向,再去学习SpringSecurity就会更加具有目的性。
SpringSecurity的配置总体来说比较复杂和繁琐,但是为了确保我们开发出的系统拥有较高的安全性,再复杂的配置也是应该的。
以上就是本文的全部内容!
文章一
文章二