参考文献:
What is authentication in Spring Security?
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
基本使用:
添加依赖:
-
<!– 安全框架 Spring Security –>
-
<dependency>
-
<groupId>org.springframework.boot
</groupId>
-
<artifactId>spring-boot-starter-security
</artifactId>
-
</dependency>
这里有一篇博客入门学习很不错:Spring boot 中 Spring Security 使用改造5部曲
我的项目中的使用:
自定义的User对象:
-
/**
-
* 自定义的 User 对象
-
* 此 User 类不是我们的数据库里的用户类,是用来安全服务的
-
*/
-
public
class AnyUser extends User {
-
//import org.springframework.security.core.userdetails.User;
-
-
private Long id;
-
-
private String nickname;
-
-
AnyUser(
-
String username,
-
String password,
-
Collection<? extends GrantedAuthority> authorities
-
) {
-
super(username, password, authorities);
-
}
-
-
public Long getId() {
-
return id;
-
}
-
-
public void setId(Long id) {
-
this.id = id;
-
}
-
-
public String getNickname() {
-
return nickname;
-
}
-
-
public void setNickname(String nickname) {
-
this.nickname = nickname;
-
}
-
}
继承UserDetailsService:
首先这里我们需要重写UserDetailsService接口,然后实现该接口中的loadUserByUsername方法,通过该方法查询到对应的用户,这里之所以要实现UserDetailsService接口,是因为在Spring Security中我们配置相关参数需要UserDetailsService类型的数据。
Spring Security 支持把权限划分层次,高层次包含低层次的权限,比如 `ROLE_AMDIN,ROLE_USER`两个权限,若用户拥有了ROLE_AMDIN权限,那么相当于有了ROLE_USER权限。用户被授权了ADMIN,那么就相当于有其他所有的权限。
-
/**
-
* 自定义 UserDetailsService
-
*/
-
@Service
-
class AnyUserDetailsService implements UserDetailsService {
-
-
private
final UserService userService;
-
-
public AnyUserDetailsService(UserService userService){
-
this.userService = userService;
-
}
-
-
@Override
-
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
-
com.zhou.model.User user = userService.getByEmail(s);
-
if (user ==
null){
-
throw
new UsernameNotFoundException(
"用户不存在");
-
}
-
List<SimpleGrantedAuthority> authorities =
new ArrayList<>();
-
//对应的权限添加
-
authorities.add(
new SimpleGrantedAuthority(
"ROLE_USER"));
-
AnyUser anyUser =
new AnyUser(s, user.getPassword(), authorities);
-
anyUser.setId(user.getId());
-
anyUser.setNickname(user.getNickname());
-
return anyUser;
-
}
-
-
}
安全控制中心:
-
/**
-
* 安全控制中心
-
*/
-
@EnableWebSecurity
//@EnableWebMvcSecurity 注解开启Spring Security的功能
-
public
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
-
-
private
final UserDetailsService userDetailsService;
-
-
public WebSecurityConfig(AnyUserDetailsService userDetailsService){
-
this.userDetailsService = userDetailsService;
-
}
-
-
@Override
-
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
-
auth.userDetailsService(
this.userDetailsService);
-
}
-
-
/**
-
* http.authorizeRequests()
-
.anyRequest().authenticated()
-
.and().formLogin().loginPage("/login")
-
//设置默认登录成功跳转页面
-
.defaultSuccessUrl("/index").failureUrl("/login?error").permitAll()
-
.and()
-
//开启cookie保存用户数据
-
.rememberMe()
-
//设置cookie有效期
-
.tokenValiditySeconds(60 * 60 * 24 * 7)
-
//设置cookie的私钥
-
.key("")
-
.and()
-
.logout()
-
//默认注销行为为logout,可以通过下面的方式来修改
-
.logoutUrl("/custom-logout")
-
//设置注销成功后跳转页面,默认是跳转到登录页面
-
.logoutSuccessUrl("")
-
.permitAll();
-
* @param http
-
* @throws Exception
-
*/
-
@Override
-
protected void configure(HttpSecurity http) throws Exception {
-
http
-
.authorizeRequests()
//authorizeRequests() 定义哪些URL需要被保护、哪些不需要被保护
-
.antMatchers(
"/user/**",
"/news/**").authenticated()
-
.anyRequest().permitAll()
-
.and()
-
.formLogin()
-
.loginPage(
"/login")
-
.defaultSuccessUrl(
"/user",
true)
-
.permitAll()
-
.and()
-
.logout()
-
.permitAll()
-
.and().csrf().disable();
-
}
-
-
}
Spring Security提供了一个过滤器来拦截请求并验证用户身份。如果用户身份认证失败,页面就重定向到/login?error,并且页面中会展现相应的错误信息。若用户想要注销登录,可以通过访问@{/logout}请求,在完成注销之后,页面展现相应的成功消息。
自定义登录成功处理逻辑:
使登陆成功后跳到登录前页面:
-
//处理登录成功的。
-
@Component(
"myAuthenticationSuccessHandler")
-
public
class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
-
-
@Autowired
-
private ObjectMapper objectMapper;
-
-
@Override
-
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
-
throws IOException, ServletException {
-
//什么都不做的话,那就直接调用父类的方法
-
super.onAuthenticationSuccess(request, response, authentication);
-
-
String url=request.getRequestURI();
-
-
//如果是要跳转到某个页面的
-
new DefaultRedirectStrategy().sendRedirect(request, response, url);
-
-
}
-
}
重新配置安全中心(代码完成之后,修改配置config类代码。添加2个注解,自动注入):
-
@Autowired
-
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
-
@Override
-
protected void configure(HttpSecurity http) throws Exception {
-
http
-
.authorizeRequests()
//authorizeRequests() 定义哪些URL需要被保护、哪些不需要被保护
-
.antMatchers(
"/user/**",
"/news/**",
"/blog/manage/**",
"/blog/create/**").authenticated()
-
.anyRequest().permitAll()
-
.and()
-
.formLogin()
-
.loginPage(
"/login")
-
.successHandler(myAuthenticationSuccessHandler)
//登陆成功处理
-
.permitAll()
-
.and()
-
.logout()
-
.permitAll()
-
.and().csrf().disable();
-
}
QQ登录实现:
准备工作:
为了方便各位测试,这里直接提供一个可以使用的:
APP ID:101386962
APP Key:2a0f820407df400b84a854d054be8b6a
提醒:因为回调地址不是 http://localhost ,所以在启动我提供的demo时,需要在host文件中添加一行:127.0.0.1 www.ictgu.cn
后端详解:
1、自定义 QQAuthenticationFilter 继承 AbstractAuthenticationProcessingFilter:
-
import com.alibaba.fastjson.JSON;
-
import org.jsoup.Jsoup;
-
import org.jsoup.nodes.Document;
-
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-
import org.springframework.security.core.Authentication;
-
import org.springframework.security.core.AuthenticationException;
-
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
-
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
-
-
import javax.servlet.ServletException;
-
import javax.servlet.http.HttpServletRequest;
-
import javax.servlet.http.HttpServletResponse;
-
import java.io.IOException;
-
import java.util.regex.Matcher;
-
import java.util.regex.Pattern;
-
-
public
class QQAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
-
private
final
static String CODE =
“code”;
-
-
/**
-
* 获取 Token 的 API
-
*/
-
private
final
static String accessTokenUri =
“https://graph.qq.com/oauth2.0/token”;
-
-
/**
-
* grant_type 由腾讯提供
-
*/
-
private
final
static String grantType =
“authorization_code”;
-
-
/**
-
* client_id 由腾讯提供
-
*/
-
public
static
final String clientId =
“101386962”;
-
-
/**
-
* client_secret 由腾讯提供
-
*/
-
private
final
static String clientSecret =
“2a0f820407df400b84a854d054be8b6a”;
-
-
/**
-
* redirect_uri 腾讯回调地址
-
*/
-
private
final
static String redirectUri =
“http://www.ictgu.cn/login/qq”;
-
-
/**
-
* 获取 OpenID 的 API 地址
-
*/
-
private
final
static String openIdUri =
“https://graph.qq.com/oauth2.0/me?access_token=”;
-
-
/**
-
* 获取 token 的地址拼接
-
*/
-
private
final
static String TOKEN_ACCESS_API =
“%s?grant_type=%s&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s”;
-
-
public QQAuthenticationFilter(String defaultFilterProcessesUrl) {
-
super(
new AntPathRequestMatcher(defaultFilterProcessesUrl,
“GET”));
-
}
-
-
@Override
-
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
-
String code = request.getParameter(CODE);
-
String tokenAccessApi = String.format(TOKEN_ACCESS_API, accessTokenUri, grantType, clientId, clientSecret, code, redirectUri);
-
QQToken qqToken =
this.getToken(tokenAccessApi);
-
if (qqToken !=
null){
-
String openId = getOpenId(qqToken.getAccessToken());
-
if (openId !=
null){
-
// 生成验证 authenticationToken
-
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(qqToken.getAccessToken(), openId);
-
// 返回验证结果
-
return
this.getAuthenticationManager().authenticate(authRequest);
-
}
-
}
-
return
null;
-
}
-
-
private QQToken getToken(String tokenAccessApi) throws IOException{
-
Document document = Jsoup.connect(tokenAccessApi).get();
-
String tokenResult = document.text();
-
String[] results = tokenResult.split(
“&”);
-
if (results.length ==
3){
-
QQToken qqToken =
new QQToken();
-
String accessToken = results[
0].replace(
“access_token=”,
“”);
-
int expiresIn = Integer.valueOf(results[
1].replace(
“expires_in=”,
“”));
-
String refreshToken = results[
2].replace(
“refresh_token=”,
“”);
-
qqToken.setAccessToken(accessToken);
-
qqToken.setExpiresIn(expiresIn);
-
qqToken.setRefresh_token(refreshToken);
-
return qqToken;
-
}
-
return
null;
-
}
-
-
private String getOpenId(String accessToken) throws IOException{
-
String url = openIdUri + accessToken;
-
Document document = Jsoup.connect(url).get();
-
String resultText = document.text();
-
Matcher matcher = Pattern.compile(
“\”openid\”:\”(.*?)\”“).matcher(resultText);
-
if (matcher.find()){
-
return matcher.group(
1);
-
}
-
return
null;
-
}
-
-
class QQToken {
-
-
/**
-
* token
-
*/
-
private String accessToken;
-
-
/**
-
* 有效期
-
*/
-
private
int expiresIn;
-
-
/**
-
* 刷新时用的 token
-
*/
-
private String refresh_token;
-
-
String getAccessToken() {
-
return accessToken;
-
}
-
-
void setAccessToken(String accessToken) {
-
this.accessToken = accessToken;
-
}
-
-
public int getExpiresIn() {
-
return expiresIn;
-
}
-
-
void setExpiresIn(int expiresIn) {
-
this.expiresIn = expiresIn;
-
}
-
-
public String getRefresh_token() {
-
return refresh_token;
-
}
-
-
void setRefresh_token(String refresh_token) {
-
this.refresh_token = refresh_token;
-
}
-
}
-
}
说明:Filter 过滤时执行的方法是 doFilter(),由于 QQAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter,所以过滤时使用的是父类的doFilter() 方法。
说明:doFilter()方法中,有一步是 attemptAuthentication(request, response) 即为 QQAuthenticationFilter 中实现的方法。这个方法中调用了 this.getAuthenticationManager().authenticate(authRequest),这里自定义了类 QQAuthenticationManager,代码如下:
-
import com.alibaba.fastjson.JSON;
-
import com.alibaba.fastjson.JSONObject;
-
import com.zhou.model.User;
-
import org.jsoup.Jsoup;
-
import org.jsoup.nodes.Document;
-
import org.springframework.security.authentication.AuthenticationManager;
-
import org.springframework.security.authentication.BadCredentialsException;
-
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.core.authority.SimpleGrantedAuthority;
-
import java.io.IOException;
-
import java.util.ArrayList;
-
import java.util.List;
-
-
import
static com.zhou.config.qq.QQAuthenticationFilter.clientId;
-
-
public
class QQAuthenticationManager implements AuthenticationManager {
-
private
static
final List<GrantedAuthority> AUTHORITIES =
new ArrayList<>();
-
-
/**
-
* 获取 QQ 登录信息的 API 地址
-
*/
-
private
final
static String userInfoUri =
“https://graph.qq.com/user/get_user_info”;
-
-
/**
-
* 获取 QQ 用户信息的地址拼接
-
*/
-
private
final
static String USER_INFO_API =
“%s?access_token=%s&oauth_consumer_key=%s&openid=%s”;
-
-
static {
-
AUTHORITIES.add(
new SimpleGrantedAuthority(
“ROLE_USER”));
-
}
-
-
@Override
-
public Authentication authenticate(Authentication auth) throws AuthenticationException {
-
if (auth.getName() !=
null && auth.getCredentials() !=
null) {
-
User user =
null;
-
try {
-
user = getUserInfo(auth.getName(), (String) (auth.getCredentials()));
-
}
catch (Exception e) {
-
e.printStackTrace();
-
}
-
return
new UsernamePasswordAuthenticationToken(user,
-
null, AUTHORITIES);
-
}
-
throw
new BadCredentialsException(
“Bad Credentials”);
-
}
-
-
private User getUserInfo(String accessToken, String openId) throws Exception {
-
String url = String.format(USER_INFO_API, userInfoUri, accessToken, clientId, openId);
-
Document document;
-
try {
-
document = Jsoup.connect(url).get();
-
}
catch (IOException e) {
-
throw
new BadCredentialsException(
“Bad Credentials!”);
-
}
-
String resultText = document.text();
-
JSONObject json = JSON.parseObject(resultText);
-
-
User user =
new User();
-
user.setNickname(json.getString(
“nickname”));
-
user.setEmail(
“暂无。。。。”);
-
//user.setGender(json.getString(“gender”));
-
//user.setProvince(json.getString(“province”));
-
//user.setYear(json.getString(“year”));
-
user.setAvatar(json.getString(
“figureurl_qq_2”));
-
-
return user;
-
}
说明:QQAuthenticationManager 的作用是通过传来的 token 和 openID 去请求腾讯的getUserInfo接口,获取腾讯用户的信息,并生成新的 Authtication 对象。
接下来就是要将 QQAuthenticationFilter 与 QQAuthenticationManager 结合,配置到 Spring Security 的过滤器链中。代码如下:
-
@Override
-
protected void configure(HttpSecurity http) throws Exception {
-
http
-
.authorizeRequests()
//authorizeRequests() 定义哪些URL需要被保护、哪些不需要被保护
-
.antMatchers(
“/user/**”,
“/news/**”,
“/blog/manage/**”).authenticated()
-
.anyRequest().permitAll()
-
.and()
-
.formLogin()
-
.loginPage(
“/login”)
-
.successHandler(myAuthenticationSuccessHandler)
//登陆成功处理
-
.permitAll()
-
.and()
-
.logout()
-
.permitAll()
-
.and().csrf().disable();
-
// 在 UsernamePasswordAuthenticationFilter 前添加 QQAuthenticationFilter
-
http.addFilterAt(qqAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
-
}
-
-
/**
-
* 自定义 QQ登录 过滤器
-
*/
-
private QQAuthenticationFilter qqAuthenticationFilter(){
-
QQAuthenticationFilter authenticationFilter =
new QQAuthenticationFilter(
“/login/qq”);
-
//SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
-
//successHandler.setAlwaysUseDefaultTargetUrl(true);
-
//successHandler.setDefaultTargetUrl(“/user”);
-
MyAuthenticationSuccessHandler successHandler =
new MyAuthenticationSuccessHandler();
-
authenticationFilter.setAuthenticationManager(
new QQAuthenticationManager());
-
authenticationFilter.setAuthenticationSuccessHandler(successHandler);
-
return authenticationFilter;
-
}
说明:由于腾讯的回调地址是 /login/qq,所以 QQAuthenticationFilter 拦截的路径是 /login/qq,然后将 QQAuthenticationFilter 置于 UsernamePasswordAuthenticationFilter 相同级别的位置。
前端说明:
前端很简单,一个QQ登陆按钮,代码如下:<a href="https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101386962&redirect_uri=http://www.ictgu.cn/login/qq&state=test" class="btn btn-primary btn-block">QQ登录</a>
其他说明:
腾讯官网原话:openid是此网站上唯一对应用户身份的标识,网站可将此ID进行存储便于用户下次登录时辨识其身份,或将其与用户在网站上的原有账号进行绑定。
通过QQ登录获取的 openid 用于与自己网站的账号一一对应。
转自: https://blog.csdn.net/qq_35508033/article/details/79046441