学习Spring Security 的认证和授权。本文主要介绍在前后端分离开发的情况下的使用。
一、Spring Security 的依赖二、新建控制器类org.springframework.boot spring-boot-starter-security
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@GetMapping("/hello")
public Object sayHello(){
return "hello world";
}
}
三、访问请求
项目启动成功后, 用浏览器访问 http://localhost:8080/hello
正常情况下浏览器会打印出来 “hello world” 字符串。但是我们因为我们引入了 Spring Security 依赖。浏览器会自动跳转到 http://localhost:8080/login 。如下图:
思路分析
登录
1.准备登录接口,参数是账号和密码。调用ProviderManager的方法进行验证,。如果验证通过把用户信息存入Redis,键为id。根据id生成JWT响应给前端。
2.自定义userDetailsService类的实现类.在这个类中查询数据库。
校验(即如何获取用户信息思路)
1.创建JWT解析过滤器
获取token
解析token。获取id
根据id在Redis中获取用户信息
用户信息存入在SecurityContextHolder中。
org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-data-redis com.alibaba fastjson 1.2.58 io.jsonwebtoken jjwt 0.9.1
整合Redis
spring.redis.database=
spring.redis.host=
spring.redis.port=6379
spring.redis.password=
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 设置key的序列化方式
template.setKeySerializer(RedisSerializer.string());
// 设置value的序列化方式
template.setValueSerializer(RedisSerializer.json());
// 设置hash的key的序列化方式
template.setHashKeySerializer(RedisSerializer.string());
// 设置hash的value的序列化方式
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
@Data
public class Result {
private int code;
private String msg;
private Object data;
public Result(){
}
public Result(int code, String msg,Object data){
this.code=code;
this.data=data;
this.msg=msg;
}
public Result(int code, String msg){
this.code=code;
this.msg=msg;
}
public Result(int code, Object date){
this.code=code;
this.data=date;
}
public Result(int code){
this.code=code;
}
}
public class JwtUtils {
private static final int TOKEN_TIME_OUT = 3_600*24*7;
private static final String TOKEN_ENCRY_KEY = "MDk4ZjjZiY20NjIxDM3kM2NhZU0ZTgzjMjYyN2I0ZjY";
public static String getToken(Integer userId){
long currentTime = System.currentTimeMillis();
//头信息
return Jwts.builder().setHeaderParam("typ","JWT")
.setHeaderParam("alg","HS256")
//载荷
.claim("userId",userId)
//过期时间戳
.setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000))
//jwt编号:随机产生
.setId(UUID.randomUUID().toString())
//签名
.signWith(SignatureAlgorithm.HS256, TOKEN_ENCRY_KEY)
.compact();
}
private static Jws getJws(String token) {
return Jwts.parser()
.setSigningKey(TOKEN_ENCRY_KEY)
.parseClaimsJws(token);
}
public static Claims getClaims(String token) {
try {
return getJws(token).getBody();
}catch (ExpiredJwtException e){
return null;
}
}
public static Integer checkToken(String token) {
try {
Claims claims = getClaims(token);
if(claims==null){
throw new RuntimeException("token解析失败");
}
return (Integer) claims.get("userId");
} catch (ExpiredJwtException ex) {
throw new RuntimeException("token已经失效");
}catch (Exception e){
throw new RuntimeException("token解析失败");
}
}
}
六、认证的代码实现
自定义userDetailsService类的实现类.在这个类中查询数据库。把查询到的用户信息封装成UserDetails类
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
User user = userMapper.selectOne(new QueryWrapper().eq("qq", username));
if (Objects.isNull(user)){
throw new RuntimeException("账号错误或者密码错误");
}
//查询用户授权信息(待完成)
return new LoginUser(user);
}
}
返回结果是UserDetails 类。这个是接口,需要实现。
@Data
public class LoginUser implements UserDetails {
private User user;
public LoginUser(User user){
this.user=user;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String 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;
}
}
Spring Security 默认有密码加密格式。这里需要替换成BCryptPasswordEncoder。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
这个对象可以对密码进行加密。可以对明文和加密密码进行校验。
定义登录接口
配置类中加入AuthenticationManager (内部调用UserDetailsService 的实现类,会自动校验账号和密码)和 重写configure(登录接口放行)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF禁用
// 从Spring Security 4.0开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用程序,Spring Security CSRF会针对PATCH,POST,PUT和DELETE方法进行防护。
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 过滤请求
.and()
.authorizeRequests()
//允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
}
@PostMapping(path = "/login")
public Result login(@RequestBody User user){
//获取前端输入的qq和密码
String qq = user.getQq();
String password = user.getPassword();
//把前端账号和密码封装成Authentication
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(qq,password);
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
if (Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
//用户信息保存在Redis
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
User u = loginUser.getUser();
Integer id = u.getId();
u.setPassword(null);
u.setCreateTime(null);
u.setUpdateTime(null);
redisTemplate.opsForValue().set("login:"+id,u);
//认证通过,根据userId生成token。
String token = JwtUtils.getToken(id);
return new Result(200,"登录成功",token);
}
七、校验过滤器的代码实现(解析token)
@Component
public class JwtTokenFilter 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.isBlank(token)){
//放行
filterChain.doFilter(request,response);
return;
}
//解析token
Integer id=0;
try {
id = JwtUtils.checkToken(token);
}catch (Exception e){
// 将错误信息封装在request中
request.setAttribute("exception", e);
// 请求转发
request.getRequestDispatcher("/filterGlobalException").forward(request, response);
}
//从Redis中获取用户信息
User user = (User) redisTemplate.opsForValue().get("login:" + id);
if (Objects.isNull(user)){
// 将错误信息封装在request中
request.setAttribute("exception", new Exception("用户未登录!"));
// 请求转发
request.getRequestDispatcher("/filterGlobalException").forward(request, response);
}
// 用户信息存入在SecurityContextHolder中。
UsernamePasswordAuthenticationToken authenticationToken=
new UsernamePasswordAuthenticationToken(user,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response);
}
}
Spring Security有一系列的过滤器。 需要配置该过滤器的顺序
@Autowired
private JwtTokenFilter jwtTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF禁用
// 从Spring Security 4.0开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用程序,Spring Security CSRF会针对PATCH,POST,PUT和DELETE方法进行防护。
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 过滤请求
.and()
.authorizeRequests()
//允许匿名访问
.antMatchers("/login")
.anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//配置过滤器
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
八、用户身份权限(简单版用户角色String role)
@Service
public class UserDetailsServiceImpl implements UserDetailsService {//452014375
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
User user = userMapper.selectOne(new QueryWrapper().eq("qq", username));
if (Objects.isNull(user)){
throw new RuntimeException("账号或者密码错误!");
}
//查询用户授权信息(user类中有一个String role字段表示用户身份)
return new LoginUser(user);
}
}
这一步增加了根据user对象的role属性,getAuthorities()方法封装了用户权限信息。
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
public LoginUser(User user){
this.user=user;
}
@Override
@JsonIgnore
public Collection extends GrantedAuthority> getAuthorities() {
List list=new ArrayList<>();
list.add(new SimpleGrantedAuthority(user.getRole()));
return list;
}
@Override
@JsonIgnore
public String getPassword() {
return user.getPassword();
}
@Override
@JsonIgnore
public String getUsername() {
return user.getUsername();
}
@Override
@JsonIgnore
public boolean isAccountNonExpired() {
return true;
}
@Override
@JsonIgnore
public boolean isAccountNonLocked() {
return true;
}
@Override
@JsonIgnore
public boolean isCredentialsNonExpired() {
return true;
}
@Override
@JsonIgnore
public boolean isEnabled() {
return true;
}
}
这一步 UsernamePasswordAuthenticationToken authenticationToken=
new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
第三个参数是用户的权限。
@Component
public class JwtTokenFilter 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.isBlank(token)){
//放行
filterChain.doFilter(request,response);
return;
}
//解析token
Integer id=0;
try {
id = JwtUtils.checkToken(token);
}catch (Exception e){
// 将错误信息封装在request中
request.setAttribute("exception", e);
// 请求转发
request.getRequestDispatcher("/filterGlobalException").forward(request, response);
}
//从Redis中获取用户信息
LoginUser user = (LoginUser) redisTemplate.opsForValue().get("login:" + id);
if (Objects.isNull(user)){
// 将错误信息封装在request中
request.setAttribute("exception", new Exception("用户未登录!"));
// 请求转发
request.getRequestDispatcher("/filterGlobalException").forward(request, response);
}
// 用户信息存入在SecurityContextHolder中。
UsernamePasswordAuthenticationToken authenticationToken=
new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response);
}
}
@PreAuthorize(“hasAuthority(‘test’)”)注解。如果用户角色是test,那么才可以访问该接口。
@RequestMapping(path = "/hello", method = RequestMethod.POST)
@PreAuthorize("hasAuthority('test')")
public String hello() {
return "hello";
}
总结
第八章用户权限的区分管理是很简单的,单纯依赖User对象的role(角色)字段进行判断。完整的权限管理参考第九章。
完整的User类
@Data
public class User {
@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String qq;
private String password;
private String idCard;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
//用户角色
private String role;
}
九、 RBAC权限模型
RBAC权限模型
在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。(如下图)
建立以上五个表,可以更加细腻,精准的管理用户权限。比单纯的在页面进行是否有某功能按键展现的判断更加安全。



