Version: Next

用户退出

最佳实践

@Override
protected void configure(final HttpSecurity http) throws Exception {
http.logout().and().remeberMe().....;
}
  • 前端

在开启CSRF防护的情况下,需要采用POST方式访问这个路径

<a href="/logout">退出登录</a>

Logout完成的功能

  • 当前session失效,即:logout的核心需求,session失效就是访问权限的回收。
  • 删除当前用户的 remember-me“记住我”功能信息
  • clear清除当前的 SecurityContext
  • 重定向到登录页面,loginPage配置项指定的页面

个性化设置

http.logout()
.logoutUrl("/signout")
.logoutSuccessUrl("/aftersignout.html")
.deleteCookies("JSESSIONID")
  • 通过指定logoutUrl配置改变退出请求的默认路径,当然html退出按钮的请求url也要修改
  • 通过指定logoutSuccessUrl配置,来显式指定退出之后的跳转页面
  • 还可以使用deleteCookies删除指定的cookie,参数为cookie的名称

LogoutSuccessHandler

对于需要大量自定义逻辑的场景

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
//这里书写你自己的退出业务逻辑
// 重定向到登录页
response.sendRedirect("/login.html");
}
}
  • 在配置中使其生效
@Autowired
private MyLogoutSuccessHandler myLogoutSuccessHandler;
@Override
protected void configure(final HttpSecurity http) throws Exception {
http.logout()
.logoutUrl("/signout")
//.logoutSuccessUrl(``"/aftersignout.html"``)
.deleteCookies("JSESSIONID")
//自定义logoutSuccessHandler
.logoutSuccessHandler(myLogoutSuccessHandler);
}

logoutSuccessUrl不要与logoutSuccessHandler一起使用,否则logoutSuccessHandler将失效。


阶段代码总结

Config

@Configuration
@SuppressWarnings("all")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
private MyAuthenticationFaliureHandler myAuthenticationFaliureHandler;
@Resource
private MyUserDetialService myUserDetialService;
@Resource
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.logout()
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.and()
.csrf().disable() //禁用跨站csrf攻击防御,后面的章节会专门讲解
.formLogin()
.loginPage("/login.html")//用户未登录时,访问任何资源都转跳到该路径,即登录页面
.loginProcessingUrl("/login")//登录表单form中action的地址,也就是处理认证请求的路径
.usernameParameter("username")///登录表单form中用户名输入框input的name名,不修改的话默认是username
.passwordParameter("password")//form中密码输入框input的name名,不修改的话默认是password
// .defaultSuccessUrl("/index")//登录认证成功后默认转跳的路径
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFaliureHandler)
.and()
.authorizeRequests()
.antMatchers("/login.html", "/login").permitAll()//不需要通过登录验证就可以被访问的资源路径
.antMatchers("/index", "/").authenticated()// 首页只要登录了就可以访问
.anyRequest().access("@rbacService.hasPermission(request, authentication)")
/* .antMatchers("/biz1", "/biz2") //需要对外暴露的资源路径
.hasAnyAuthority("ROLE_user", "ROLE_admin") //user角色和admin角色都可以访问
// .antMatchers("/syslog", "/sysuser")
// .hasAnyRole("admin") //admin角色可以访问
.antMatchers("/syslog").hasAuthority("/syslog")
.antMatchers("/sysuser").hasAuthority("/sysuser")
.anyRequest().authenticated();*/
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.inMemoryAuthentication()
// .withUser("user")
// .password(passwordEncoder().encode("123456"))
// .roles("user")
// .and()
// .withUser("admin")
// .password(passwordEncoder().encode("123456"))
// //.authorities("sys:log","sys:user")
// .roles("admin")
// .and()
// .passwordEncoder(passwordEncoder());//配置BCrypt加密
auth.userDetailsService(myUserDetialService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
@Override
public void configure(WebSecurity web) throws Exception {
//将项目中静态资源路径开放出来
web.ignoring().antMatchers("/css/**", "/fonts/**", "/img/**", "/js/**");
}
}

自定义认证响应处理

@Component
public class MyAuthenticationFaliureHandler extends SimpleUrlAuthenticationFailureHandler {
//在application配置文件中配置登陆的类型是JSON数据响应还是做页面响应
@Value("${spring.security.loginType}")
private String loginType;
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (loginType.equalsIgnoreCase("JSON")) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
objectMapper.writeValueAsString(
AjaxResponse.error(
new CustomException(
CustomExceptionType.USER_INPUT_ERROR,
"用户名或密码存在错误,请检查后再次登录"))));
} else {
response.setContentType("text/html;charset=UTF-8");
super.onAuthenticationFailure(request, response, exception);
}
}
}
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
//在application配置文件中配置登陆的类型是JSON数据响应还是做页面响应
@Value("${spring.security.loginType}")
private String loginType;
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws ServletException, IOException {
if (loginType.equalsIgnoreCase("JSON")) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(AjaxResponse.success("/index")));
} else {
// 会帮我们跳转到上一次请求的页面上
super.onAuthenticationSuccess(request, response, authentication);
}
}
}

自定义当前用户实体

public class MyUserDetails implements UserDetails {
String password; //密码
String username; //用户名
boolean accountNonExpired; //是否没过期
boolean accountNonLocked; //是否没被锁定
boolean credentialsNonExpired; //是否没过期
boolean enabled; //账号是否可用
Collection<? extends GrantedAuthority> authorities; //用户的权限集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
}

自定义异常

public enum CustomExceptionType {
USER_INPUT_ERROR(400, "用户输入异常"),
SYSTEM_ERROR(500, "系统服务异常"),
OTHER_ERROR(999, "其他未知异常");
CustomExceptionType(int code, String typeDesc) {
this.code = code;
this.typeDesc = typeDesc;
}
private String typeDesc;//异常类型中文描述
private int code; //code
public String getTypeDesc() {
return typeDesc;
}
public int getCode() {
return code;
}
}
public class CustomException extends RuntimeException {
//异常错误编码
private int code ;
//异常信息
private String message;
private CustomException(){}
public CustomException(CustomExceptionType exceptionTypeEnum,
String message) {
this.code = exceptionTypeEnum.getCode();
this.message = message;
}
public int getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}

自定义动态查询权限

@Service("rbacService")
public class MyRBACService {
@Resource
MyRBACServiceMapper myRBACServiceMapper;
/**
* 判断某用户是否具有该request资源的访问权限
*/
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
// 获取验证主体,就是MyUserDetails
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
UserDetails userDetails = ((UserDetails)principal);
List<GrantedAuthority> authorityList =
AuthorityUtils.commaSeparatedStringToAuthorityList(request.getRequestURI());
return userDetails.getAuthorities().contains(authorityList.get(0));
}
return false;
}
}

自定义动态设置用户角色权限

@Service
public class MyUserDetialService implements UserDetailsService {
@Resource
private MyUserDetailsServiceMapper userDetailsServiceMapper;
/***
* 1. 加载用户基本信息
* 2. 加载用户角色列表
* 3. 通过用户角色列表加载用户的资源权限列表
* @param username 用户名
* @return 从数据库查出来的用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//获得用户信息
MyUserDetails myUserDetails = userDetailsServiceMapper.findByUserName(username);
if (myUserDetails == null) {
throw new UsernameNotFoundException("用户不存在");
}
//获得用户角色列表
List<String> roleCodes = userDetailsServiceMapper.findRoleByUserName(username);
//通过角色列表获取权限列表
List<String> authority = userDetailsServiceMapper.findAuthorityByRoleCodes(roleCodes);
// 角色是一种特殊的权限
//为角色标识加上ROLE_前缀(Spring Security规范)
roleCodes = roleCodes.stream()
.map(roleCode -> "ROLE_" + roleCode)
.collect(Collectors.toList());
//角色是一种特殊的权限,所以合并
authority.addAll(roleCodes);
//转成用逗号分隔的字符串,为用户设置权限标识
myUserDetails.setAuthorities(
AuthorityUtils.commaSeparatedStringToAuthorityList(
String.join(",", authority)
)
);
return myUserDetails;
}
}

统一Ajax响应

/**
* 接口数据请求统一响应数据结构
*/
@Data
public class AjaxResponse {
private boolean isok;
private int code;
private String message;
private Object data;
private AjaxResponse() {}
//请求出现异常时的响应数据封装
public static AjaxResponse error(CustomException e) {
AjaxResponse resultBean = new AjaxResponse();
resultBean.setIsok(false);
resultBean.setCode(e.getCode());
if(e.getCode() == CustomExceptionType.USER_INPUT_ERROR.getCode()){
resultBean.setMessage(e.getMessage());
}else if(e.getCode() == CustomExceptionType.SYSTEM_ERROR.getCode()){
resultBean.setMessage(e.getMessage() + ",请将该异常信息发送给管理员!");
}else{
resultBean.setMessage("系统出现未知异常,请联系管理员!");
}
//TODO 这里最好将异常信息持久化
return resultBean;
}
//请求出现异常时的响应数据封装
public static AjaxResponse error(CustomExceptionType customExceptionType,
String errorMessage) {
AjaxResponse resultBean = new AjaxResponse();
resultBean.setIsok(false);
resultBean.setCode(customExceptionType.getCode());
resultBean.setMessage(errorMessage);
return resultBean;
}
//请求处理成功时的数据响应
public static AjaxResponse success() {
AjaxResponse resultBean = new AjaxResponse();
resultBean.setIsok(true);
resultBean.setCode(200);
resultBean.setMessage("success");
return resultBean;
}
//请求处理成功,并响应结果数据
public static AjaxResponse success(Object data) {
AjaxResponse resultBean = AjaxResponse.success();
resultBean.setData(data);
return resultBean;
}
}

Controller

@Controller
public class BizpageController {
@Resource
MethodELService methodELDemo;
// @PostMapping("/login")
// public String login(String username, String password) {
// return "index";
// }
@GetMapping({"/index", "/"})
public String index() {
return "index";
}
@GetMapping("/syslog")
public String syslog() {
return "syslog";
}
@GetMapping("/sysuser")
public String sysuser() {
return "sysuser";
}
@GetMapping("/biz1")
public String biz1() {
// List<PersonDemo> allPD = methodELDemo.findAll();
return "biz1";
}
@GetMapping("/biz2")
public String biz2() {
return "biz2";
}
}

application.yaml

spring:
freemarker:
cache: false # 缓存配置 开发阶段应该配置为false 因为经常会改
suffix: .html # 模版后缀名 默认为ftl / 还是用ftl吧,html没freemarker语法提示
charset: UTF-8 # 文件编码
template-loader-path: classpath:/templates/
datasource:
username: root
password: root
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/oauth?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # 配置使用Druid数据源
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:ttps://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
security:
loginType: JSON
# 配置Mybatis
# 也可以不配置,使用全限定类名,然后在resources路径下建立和mapper接口一样的包路径
mybatis:
# type-aliases-package: com.bsx.shiro.pojo # 别名
mapper-locations: classpath:mapper/*.xml # 扫描mapper.xml文件路径
server:
port: 8081

数据库

持久化RememberMeToken

CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

用户|角色|权限

看第5节——RBAC

前端

记得在CSRF防御模式下,必须使用POST请求方式提交登录和登出即可