添加配置类SecurityConfig
SpringSecurity5.7+不需要继承WebSecurityConfigurerAdapter,而是注入一个过滤链的Bean,通过这个过滤链去处理用户登录的请求
@Bean
SecurityFilterChain filerChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests()
.anyRequest().authenticated()
.and().formLogin()
.and().csrf().disable()
.build();
}
自定义登录处理
登陆成功
-
自定义AuthenticationSuccessHandler接口的实现类
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Map<String, Object> result = new HashMap<String, Object>(); result.put("msg", "登录成功"); result.put("status", 200); response.setContentType("application/json;charset=UTF-8"); String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); } }
-
filterChain方法中替换
.and().formLogin().successHandler(new MyAuthenticationSucccessHandler())
登陆失败
-
自定义AuthenticationFailureHandler接口的实现类
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { Map<String, Object> result = new HashMap<String, Object>(); result.put("msg", "登录失败: "+exception.getMessage()); result.put("status", 500); response.setContentType("application/json;charset=UTF-8"); String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); } }
-
filterChain方法中替换
.failureHandler(new MyAuthenticationFailureHandler())
自定义登出处理
-
自定义LogoutSuccessHandler接口的实现类
public class MyLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Map<String, Object> result = new HashMap<String, Object>(); result.put("msg", "注销成功"); result.put("status", 200); response.setContentType("application/json;charset=UTF-8"); String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); } }
-
filterChain方法中替换
.and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
-
通过 logout() 方法开启注销配置
-
logoutUrl 指定退出登录请求地址,默认是 GET 请求,路径为
/logout
-
invalidateHttpSession 退出时是否使 session 失效,默认值为 true
-
clearAuthentication 退出时是否清除认证信息,默认值为 true
-
可以配置多个注销登录的请求,指定方法
.logoutRequestMatcher(new OrRequestMatcher( new AntPathRequestMatcher("/logout1","GET"), new AntPathRequestMatcher("/logout","GET") ))
-
自定义认证数据源
-
依赖
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.11</version> </dependency>
-
设计表结构
-- 用户表 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(32) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `enabled` tinyint(1) DEFAULT NULL, `accountNonExpired` tinyint(1) DEFAULT NULL, `accountNonLocked` tinyint(1) DEFAULT NULL, `credentialsNonExpired` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; -- 角色表 CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) DEFAULT NULL, `name_zh` varchar(32) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; -- 用户角色关系表 CREATE TABLE `user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `uid` int(11) DEFAULT NULL, `rid` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `uid` (`uid`), KEY `rid` (`rid`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-
加配置
# datasource spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/demo4_springsecurity spring.datasource.username=root spring.datasource.password=123456 # mybatis mybatis.mapper-locations=classpath:com/demo/mapper/*.xml mybatis.type-aliases-package=com.demo.entity # log logging.level.com.demo=debug
-
做service,dao,mapper,entity
-
Role类
package com.demo.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class Role { private Integer id; private String name; private String nameZh; //get set.. }
-
User类
package com.demo.entity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class User implements UserDetails { private Integer id; private String username; private String password; private Boolean enabled; private Boolean accountNonExpired; private Boolean accountNonLocked; private Boolean credentialsNonExpired; private List<Role> roles = new ArrayList<>(); @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); roles.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()))); return grantedAuthorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return accountNonExpired; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } public Integer getId() { return id; } public void setRoles(List<Role> roles) { this.roles = roles; } public List<Role> getRoles(){ return this.roles; } @Override public boolean isEnabled() { return enabled; } //get/set.... public void setPassword(String password) { this.password = password; } }
-
UserDao
package com.demo.dao; import com.demo.entity.Role; import com.demo.entity.User; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface UserDao { //根据用户名查询用户 User loadUserByUsername(@Param("username") String username); //根据用户id查询角色 List<Role> getRolesByUid(@Param("uid") Integer uid); }
-
UserMapper
<?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.demo.dao.UserDao"> <!--查询单个--> <select id="loadUserByUsername" resultType="com.demo.entity.User"> select id, username, password, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired from user where username = #{username} </select> <!--查询指定行数据--> <select id="getRolesByUid" resultType="com.demo.entity.Role"> select r.id, r.name, r.name_zh nameZh from role r, user_role ur where r.id = ur.rid and ur.uid = #{uid} </select> </mapper>
-
UserDetailService
package com.demo.service; import com.demo.dao.UserDao; import com.demo.entity.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; import org.springframework.util.ObjectUtils; @Service public class MyUserDetailServiceImpl implements UserDetailsService { private final UserDao userDao; @Autowired public MyUserDetailServiceImpl(UserDao userDao) { this.userDao = userDao; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.loadUserByUsername(username); if(ObjectUtils.isEmpty(user)) { throw new RuntimeException("用户不存在"); } user.setRoles(userDao.getRolesByUid(user.getId())); return user; } }
-
-
自定义LoginFilter继承UserNamePasswordFilter
package com.demo.filter; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; public class LoginFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) { try { Map<String, String> userinfo = new ObjectMapper().readValue(request.getInputStream(), Map.class); String username = userinfo.get(getUsernameParameter()); String password = userinfo.get(getPasswordParameter()); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } catch (IOException e) { e.printStackTrace(); } } return super.attemptAuthentication(request,response); } }
-
配置SecurityConfig
private final MyUserDetailServiceImpl myUserDetailService; //构造注入自定义UserDetailService @Autowired public SecurityConfig(MyUserDetailServiceImpl myUserDetailService) { this.myUserDetailService = myUserDetailService; } //注入AuthenticationManager @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } //注入过滤器 @Bean public LoginFilter loginFilter(AuthenticationManager authenticationManager){ LoginFilter loginFilter = new LoginFilter(); loginFilter.setPasswordParameter("password"); loginFilter.setUsernameParameter("username"); loginFilter.setFilterProcessesUrl("/login"); loginFilter.setAuthenticationManager(authenticationManager); loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSucccessHandler()); loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler()); return loginFilter; } //配置添加自定义认证数据源,替换过滤器 @Bean protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests() .anyRequest().authenticated() .and().formLogin() .and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler()) .and().userDetailsService(myUserDetailService) .csrf().disable(); http.addFilterAt(loginFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class); return http.build(); }
密码加密并支持旧密码自动升级
下次用户登录时密码自动升级为Bcrypt加密
-
自定义UserDetailService新增实现接口UserDetailsPasswordService
@Override public UserDetails updatePassword(UserDetails user, String newPassword) { Integer result = userDao.updatePassword(user.getUsername(), newPassword); if (result == 1) { ((User) user).setPassword(newPassword); } return user; }
-
userDao和userMapper新增对应实现
-
userDao
Integer updatePassword(@Param("username") String username,@Param("password") String password);
-
userMapper
<update id="updatePassword"> update `user` set password=#{password} where username=#{username} </update>
-
基于JWT验证的登录
JWT工具类
public class JWTUtils {
public static String TOKEN_HEADER = "token";
//过期时间
private static int EXPIRITION_DAY =7;
private static String ROLE = "role";
private static String SIGN = "#N!&SI#^";
public static String createToken(String username, List<Role> roles){
ArrayList<String> rolesList = new ArrayList<>();
for(Role role:roles){
rolesList.add(role.getName());
}
Calendar instance = Calendar.getInstance();
//过期时间设为7天
instance.add(Calendar.DATE,EXPIRITION_DAY);
String token = JWT.create()
.withArrayClaim("role",rolesList.toArray(new String[0]))
.withClaim("username",username)
.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(SIGN));
return token;
}
public static HashMap<String,Object> decode(String token){
HashMap<String, Object> map = new HashMap<>();
DecodedJWT verify;
try{
verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}catch (Exception e){
e.printStackTrace();
return map;
}
String username = verify.getClaim("username").asString();
String[] roles = verify.getClaim("role").asArray(String.class);
map.put("username",username);
map.put("roles",roles);
return map;
}
public static void setExpiritionDay(int expiritionDay) {
EXPIRITION_DAY = expiritionDay;
}
public static void setRole(String role) {
JWTUtils.ROLE = role;
}
public static void setSign(String sign) {
JWTUtils.SIGN = sign;
}
}
JWTAuthenticationFilter登录过滤器
基于之前实现的LoginFilter,在登陆成功处理中获取用户数据,返回Token
登陆成功处理Handler添加
User user = (User) authentication.getPrincipal();
String token = JWTUtils.createToken(user.getUsername(), user.getRoles());
result.put("token",token);
JWTAuthorizationFilter权限校验过滤器
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//从请求头中获取token
String token = request.getHeader(JWTUtils.TOKEN_HEADER);
//没有直接跳过过滤器
if(ObjectUtils.isEmpty(token)){
chain.doFilter(request,response);
return;
}
//将token中的用户名和权限用户组放入Authentication对象,在之后实现鉴权
SecurityContextHolder.getContext().setAuthentication(getAuthentication(token));
super.doFilterInternal(request, response, chain);
}
//解析token获取用户信息
private UsernamePasswordAuthenticationToken getAuthentication(String token){
HashMap<String, Object> tokenInfo = JWTUtils.decode(token);
if(ObjectUtils.isEmpty(tokenInfo)){
return null;
}
String username = (String) tokenInfo.get("username");
String[] roles = (String[]) tokenInfo.get("roles");
ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
for(String role:roles){
authorities.add(new SimpleGrantedAuthority(role));
}
return new UsernamePasswordAuthenticationToken(username,null,authorities);
}
}
主类配置认证过滤器bean,添加过滤器
@Bean
public JWTAuthorizationFilter jwtAuthorizationFilter(AuthenticationManager authenticationManager){
JWTAuthorizationFilter filter = new JWTAuthorizationFilter(authenticationManager);
return filter;
}
http.addFilter(jwtAuthorizationFilter(http.getSharedObject(AuthenticationManager.class)));
设置不开启session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
添加验证码
基于Redis
新增字段uuid和kaptcha
验证码和Redis依赖包
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
Redis配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-wait=100
验证码配置类
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.height", "50");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789qazwsxedcrfvtgbyhnujmikolpQAZWSXEDCRFVTGBYHNUJMIKOLP");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
获取验证码控制器
@RestController
public class KaptchaController {
private final String redisParam = "demo:verifyCode:uuid:";
private final Producer producer;
private final StringRedisTemplate stringRedisTemplate;
@Autowired
public KaptchaController(Producer producer,StringRedisTemplate stringRedisTemplate) {
this.producer = producer;
this.stringRedisTemplate=stringRedisTemplate;
}
@RequestMapping("/vc.png")
public HashMap<String,String> getVerifyCode() throws IOException {
//1.生成验证码和标识UUID
String code = producer.createText();
String uuid = UUID.randomUUID().toString();
//redis存放验证码,UUID,过期时长五分钟
stringRedisTemplate.opsForValue().set(redisParam+uuid,code,5L, TimeUnit.MINUTES);
BufferedImage bi = producer.createImage(code);
//2.写入内存
FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
ImageIO.write(bi, "png", fos);
System.out.println(code);
//3.生成 base64,返回数据
HashMap<String, String> ret = new HashMap<>();
ret.put("uuid",uuid);
ret.put("picture",Base64.encodeBase64String(fos.toByteArray()));
return ret;
}
}
验证码异常类
public class KaptchaNotMatchException extends AuthenticationException {
public KaptchaNotMatchException(String msg) {
super(msg);
}
public KaptchaNotMatchException(String msg, Throwable cause) {
super(msg, cause);
}
}
在登录验证过滤器实现校验验证码
以下为JWT登录校验的实现
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final String redisParam = "demo:verifyCode:uuid:";
public static final String FORM_KAPTCHA_KEY = "kaptcha";
public static final String FORM_UUID_KEY = "uuid";
private String kaptcha=FORM_KAPTCHA_KEY;
private String uuid = FORM_UUID_KEY;
public String getuuidParamter() {
return uuid;
}
public void setKaptchaParamter(String kaptcha) {
this.kaptcha = kaptcha;
}
public void setuuidParamter(String uuid) {
this.uuid = uuid;
}
public String getKaptchaParamter() {
return kaptcha;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
try {
Map<String, String> userinfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String username = userinfo.get(getUsernameParameter());
String password = userinfo.get(getPasswordParameter());
String kaptcha = userinfo.get(getKaptchaParamter());
String uuid = userinfo.get(getuuidParamter());
String kaptchaRedis = stringRedisTemplate.opsForValue().get(redisParam + uuid);
if(!ObjectUtils.isEmpty(kaptchaRedis)&&!ObjectUtils.isEmpty(kaptcha)&&kaptchaRedis.equals(kaptcha)){
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
} catch (IOException e) {
e.printStackTrace();
}
}
throw new KaptchaNotMatchException("验证码错误或过期");
}
}
实现Remember-me
-
LoginFilter新增rememberme字段
String rememberValue = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER); if (!ObjectUtils.isEmpty(rememberValue)) { request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberValue); }
-
自定义RemembermeService实现类
public class PersistentTokenBasedRememberMeServicesImpl extends PersistentTokenBasedRememberMeServices { public PersistentTokenBasedRememberMeServicesImpl(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) { super(key, userDetailsService, tokenRepository); } @Override protected boolean rememberMeRequested(HttpServletRequest request, String parameter) { String paramValue = request.getAttribute(parameter).toString(); if (paramValue != null) { if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) { return true; } } return false; } }
-
配置SecurityConfig
//持久化到数据库 @Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setCreateTableOnStartup(false); //只需要没有表时设置为 true jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; } //设置默认的remembermeService @Bean public RememberMeServices rememberMeServices() { return new PersistentTokenBasedRememberMeServicesImpl(UUID.randomUUID().toString(), myUserDetailService, persistentTokenRepository()); }
-
http配置添加
.rememberMe().rememberMeServices(rememberMeServices())
-
LoginFilter配置添加
loginFilter.setRememberMeServices(rememberMeServices());
-
Remember-me数据库表也可以手动创建
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
-
开启会话管理
基于JWT进行登录鉴权不需要
-
会话过期处理
public class MyInvalidSessionStrategy implements InvalidSessionStrategy { @Override public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("application/json;charset=UTF-8"); Map<String, Object> result = new HashMap<>(); result.put("status", HttpStatus.UNAUTHORIZED.value()); result.put("msg", "当前会话已经过期,请重新登录"); String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); } }
-
http配置添加
.and().sessionManagement().invalidSessionStrategy(new MyInvalidSessionStrategy())
-
配置类注入
@Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); }
-
会话并发管理由于自定义过滤器和session不一致问题暂无解决
跨域配置
-
配置类配置跨域
CorsConfigurationSource configurationSource() { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedHeaders(Arrays.asList("*")); corsConfiguration.setAllowedMethods(Arrays.asList("*")); corsConfiguration.setAllowedOrigins(Arrays.asList("*")); corsConfiguration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", corsConfiguration); return source; }
-
http开启跨域
.cors().configurationSource(configurationSource());
自定义异常处理
- http配置添加**.exceptionHandling()**,在之后进行配置
未登录处理
配置**.authenticationEntryPoint()**
实现类:
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("msg", "未登录");
result.put("status", 401);
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
权限异常处理
配置**.accessDeniedHandler()**
实现类:
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException{
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("msg", "权限不足 ");
result.put("status", 403);
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
授权
基于URL
在http配置中的**authorizeHttpRequests()**后配置
.antMatchers("/*").hasRole("ADMIN")
权限表达式
方法 | 说明 |
---|---|
hasAuthority(String authority) | 当前用户是否具备指定权限 |
hasAnyAuthority(String… authorities) | 当前用户是否具备指定权限中任意一个 |
hasRole(String role) | 当前用户是否具备指定角色 |
hasAnyRole(String… roles); | 当前用户是否具备指定角色中任意一个 |
permitAll(); | 放行所有请求/调用 |
denyAll(); | 拒绝所有请求/调用 |
isAnonymous(); | 当前用户是否是一个匿名用户 |
isAuthenticated(); | 当前用户是否已经认证成功 |
isRememberMe(); | 当前用户是否通过 Remember-Me 自动登录 |
isFullyAuthenticated(); | 当前用户是否既不是匿名用户又不是通过 Remember-Me 自动登录的 |
hasPermission(Object targetId, Object permission); | 当前用户是否具备指定目标的指定权限信息 |
hasPermission(Object targetId, String targetType, Object permission); | 当前用户是否具备指定目标的指定权限信息 |
基于方法
配置类注解**@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true, jsr250Enabled=true)**
- perPostEnabled: 开启 Spring Security 提供的四个权限注解,@PostAuthorize、@PostFilter、@PreAuthorize 以及@PreFilter。
- securedEnabled: 开启 Spring Security 提供的 @Secured 注解支持,该注解不支持权限表达式
- jsr250Enabled: 开启 JSR-250 提供的注解,主要是@DenyAll、@PermitAll、@RolesAll 同样这些注解也不支持权限表达式
以上注解含义如下
- @PostAuthorize: 在目前标方法执行之后进行权限校验。
- @PostFiter: 在目标方法执行之后对方法的返回结果进行过滤。
- @PreAuthorize:在目标方法执行之前进行权限校验。
- @PreFiter:在目前标方法执行之前对方法参数进行过滤。
- @Secured:访问目标方法必须具各相应的角色。
- @DenyAll:拒绝所有访问。
- @PermitAll:允许所有访问。
- @RolesAllowed:访问目标方法必须具备相应的角色。
在对应Controller方法或类上添加注解,如果数据库里存的权限是ROLE_admin,则基于url只需要admin,基于方法需要写全ROLE_admin
@Secured("ROLE_admin")
@RequestMapping("/t")
public String t(HttpServletRequest request){
return "111";
}
其他用法
@PreAuthorize("hasRole('ADMIN') and authentication.name=='root'")
@PreAuthorize("authentication.name==#name")
@PreFilter(value = "filterObject.id%2!=0",filterTarget = "users")
@PostAuthorize("returnObject.id==1")
@PostFilter("filterObject.id%2==0")
@Secured({"ROLE_ADMIN","ROLE_USER"}) //具有其中一个即可
@RolesAllowed({"ROLE_ADMIN","ROLE_USER"}) //具有其中一个角色即可
OAuth2第三方登录
在对应开发者平台申请appid,appsecret
SpringSecurity通过OAuth2AuthorizationRequestRedirectFilter做第三方认证重定向,数据用Session保存,之后在OAuth2LoginAuthenticationFilter拦截用户同意登录后回调的地址,获取用户信息。
由于token验证是无状态式的,关闭了session,所以需要对oauth2登录完以后再生成我们的token用以鉴定是否登录,此处重写登录成功Handler
前端请求/oauth2/authorization/gitee,后端自动为用户重定向到gitee认证,用户认证后重定向请求后端/login/oauth2/code/gitee,获取code,后端再去第三方授权服务器拿token,再用token去拿info
从gitee获取的用户信息样例
{gists_url=https://gitee.com/api/v5/users/Loli_Wolf/gists{/gist_id}, repos_url=https://gitee.com/api/v5/users/Loli_Wolf/repos, following_url=https://gitee.com/api/v5/users/Loli_Wolf/following_url{/other_user}, bio=null, created_at=2022-11-13T12:41:11+08:00, remark=, login=Loli_Wolf, type=User, blog=https://github.com/LoliWolf, subscriptions_url=https://gitee.com/api/v5/users/Loli_Wolf/subscriptions, weibo=null, updated_at=2022-12-06T20:00:45+08:00, id=11995789, public_repos=0, email=null, organizations_url=https://gitee.com/api/v5/users/Loli_Wolf/orgs, starred_url=https://gitee.com/api/v5/users/Loli_Wolf/starred{/owner}{/repo}, followers_url=https://gitee.com/api/v5/users/Loli_Wolf/followers, public_gists=0, url=https://gitee.com/api/v5/users/Loli_Wolf, received_events_url=https://gitee.com/api/v5/users/Loli_Wolf/received_events, watched=0, followers=0, avatar_url=https://gitee.com/assets/no_portrait.png, events_url=https://gitee.com/api/v5/users/Loli_Wolf/events{/privacy}, html_url=https://gitee.com/Loli_Wolf, following=0, name=Loli_Wolf, stared=1}
oauth2配置样例
spring.security.oauth2.client.registration.gitee.client-id=
spring.security.oauth2.client.registration.gitee.client-secret=
spring.security.oauth2.client.registration.gitee.redirect-uri=http://localhost:8080/login/oauth2/code/gitee
spring.security.oauth2.client.registration.gitee.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.gitee.client-authentication-method=POST
spring.security.oauth2.client.registration.gitee.client-name=gitee
spring.security.oauth2.client.registration.gitee.provider=gitee
spring.security.oauth2.client.provider.gitee.authorization-uri=https://gitee.com/oauth/authorize
spring.security.oauth2.client.provider.gitee.user-info-authentication-method=GET
spring.security.oauth2.client.provider.gitee.token-uri=https://gitee.com/oauth/token
spring.security.oauth2.client.provider.gitee.user-info-uri=https://gitee.com/api/v5/user
spring.security.oauth2.client.provider.gitee.user-name-attribute=name
HttpSecurity配置里开启oauth2
.and().oauth2Login().successHandler(new OAuth2AuthenticationSuccessHandler(userDetailService)).failureHandler(new MyAuthenticationFailureHandler())
UserDetailService新增Oauth2用户处理
如果没有用户就创建,否则返回绑定用户
public User loadUserByGiteeID(String id, String name){
User user = userDao.loadUserByGiteeID(id);
//新用户
if (ObjectUtils.isEmpty(user)){
try {
String username = name + UUID.randomUUID();
userDao.createUserByUsername(username);
userDao.addUserGiteeInfoByUsername(username,id,name);
userDao.addUserRoleByUsername(username,"ROLE_user");
user = userDao.loadUserByGiteeID(id);
}catch (Exception e){
e.printStackTrace();
}
}
user.setRoles(userDao.getRolesByUid(user.getId()));
return user;
}
新增登陆成功控制器
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final MyUserDetailServiceImpl userDetailsService;
public OAuth2AuthenticationSuccessHandler(MyUserDetailServiceImpl myUserDetailService) {
this.userDetailsService = myUserDetailService;
}
//拿着第三方登录的用户去找user,找到就返回user生成的token,没找到就创建新user,设置user角色
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登录成功");
result.put("status", 200);
DefaultOAuth2User oAuth2User = (DefaultOAuth2User) authentication.getPrincipal();
User user = userDetailsService.loadUserByGiteeID(oAuth2User.getAttributes().get("id").toString(),oAuth2User.getAttributes().get("login").toString());
String token = JWTUtils.createToken(user.getUsername(), user.getRoles());
result.put("token",token);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
UserMapper,UserDao新增sql
<insert id="createUserByUsername">
insert into user(username, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired) value (#{username},1,1,1,1);
</insert>
<insert id="addUserGiteeInfoByUsername">
insert into user_gitee value ((select id from user where username = #{username}),#{giteeID},#{giteeName});
</insert>
<insert id="addUserRoleByUsername">
insert into user_role(uid,rid) value ((select id from user where username=#{username}),(select id from role where name = #{role}));
</insert>
<select id="loadUserByGiteeID" resultType="com.demo.security.entity.User">
select user.* from user,user_gitee ug where #{giteeID} = ug.gitee_id and ug.uid = user.id;
</select>
//根据giteeid查用户
User loadUserByGiteeID(@Param("giteeID") String giteeID);
//根据用户名创建新用户
Integer createUserByUsername(@Param("username") String username);
//绑定用户和gitee信息
Integer addUserGiteeInfoByUsername(@Param("username") String username,@Param("giteeID") String giteeID,@Param("giteeName") String giteeName);
//按用户名添加角色
Integer addUserRoleByUsername(@Param("username") String username,@Param("role") String role);